diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fe790ae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + # The verify chain launches both Chromium and WebKit. + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium webkit + + # Run unit tests against the committed dist first, so the + # "dist === fresh compile" freshness gate stays meaningful + # (a preceding build would mask a stale committed dist). + - name: Unit tests + run: npm test + + - name: Browser verify chain + run: npm run verify diff --git a/.gitignore b/.gitignore index a956bc4..ba65814 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ node_modules/ npm-debug.log* .DS_Store +# Generated debug harness (maintainer-only dev tool, not shipped). +# Sources live in scripts/debug/; regenerate with `npm run debug`. +/debug/ + # Note: dist/ and demo/styles.css ARE tracked on purpose — they're the # shipped (npm) and served (demo) artifacts. They're regenerated by # `npm run build` and `npm run build:demo`, and prepublishOnly rebuilds diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..62e205b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +All notable changes to this project are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres +to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.7.0] - 2026-06-28 + +Breaking rename to a plain direction API. [MIGRATING.md](./MIGRATING.md) is an +agent-ready upgrade procedure (ordered renames, RTL caveats, and a verification grep); +it also explains the [naming rationale](./MIGRATING.md#why-plain-directions). + +### Added + +- Direction-aware horizontal fades: `fade-start` / `fade-end` route to the correct + physical edge per the container's `:dir()`, so they flip automatically under RTL. +- `fade-none` / `fade-always` (and `-x` / `-y` axis variants) to force or disable + the active fade amount. +- Per-edge `fade-size-*` and `fade-clear-*` for `top` / `bottom` / `start` / `end`. + +### Changed + +- **Breaking:** the public API is now plain directions — `fade`, `fade-y`, `fade-top`, + `fade-bottom`, `fade-x`, `fade-start`, `fade-end` — replacing the old physical + `fade-t` / `fade-b` / `fade-l` / `fade-r` / `fade-xy` set. +- **Breaking:** `fade-x` keeps its name but now fades the inline **start + end** edges + (direction-aware), not a fixed physical left + right — so it flips under RTL. + `fade-y` is unchanged (the block axis never flips with text direction). +- **Breaking:** `fade-range-*` renamed to `fade-travel-*` (and `--fade-range-*` → + `--fade-travel-*`). +- Edge transparency is now decoupled from the travel. The masked edge saturates to + fully transparent within `travel ÷ --tw-fade-onset` (default `8`) of scroll, so a + leading edge is no longer hard-clipped while the band is still widening over the + travel. `fade-travel-*` now controls only how fast the soft band eases open (cosmetic, + safe at any size). Tune edge speed with `--tw-fade-onset`. +- The default travel is now `sm` (was `md`), for a snappier band open. +- **Breaking:** `fade-static` renamed to `fade-always`. +- The prebuilt CDN example in the README is pinned to a fixed version; unversioned + URLs track latest and will receive breaking renames. + +### Removed + +- The old physical public class names (`fade-t/b/l/r/xy`, `fade-static`, and their + `fade-size-*` / `fade-range-*` / `fade-clear-*` families). No public physical + horizontal utility (`fade-left` / `fade-right`) is provided. + +[0.7.0]: https://github.com/petekp/tw-fade/releases/tag/v0.7.0 diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 0000000..f3a6e16 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,144 @@ +# Migrating to tw-fade 0.7.0 — Plain Direction API + +This guide is for **humans and coding agents alike** — upgrade by hand, or point an agent at this file. Either way it takes a project from tw-fade `0.6.x` to `0.7.0`. + +> **Coding agents:** this document is written for you to execute. Run it top to bottom as a procedure — Step 1 (judgment calls) first, then the mechanical renames in Steps 2–3, then verify with Step 6. Don't reintroduce any removed name; the rejected shapes are deliberate and enforced by the test suite (see [Why Plain Directions](#why-plain-directions)). + +`0.7.0` is a breaking rename. Internally the engine still uses a physical four-edge mask; the public class names are now plain directions: + +```txt +fade fade-y fade-top fade-bottom fade-x fade-start fade-end +``` + +`start` / `end` are **horizontal and direction-aware**: in LTR, start = left and end = right; in RTL they swap. RTL is read from the `dir` attribute on the scroll container or an ancestor (the `:dir()` selector), not a CSS-only `direction: rtl`. + +--- + +## Step 0 — Search scope + +Search the whole project, not just `.html`. Tokens appear in `class` / `className` strings (HTML, JSX/TSX, Vue, Svelte, Astro, templates), `@apply` rules, `@theme` overrides (`--fade-range-*`), and inline custom properties (`style="--fade-clear-l: 2"`). + +**Match whole tokens only.** A token is bounded by a quote, whitespace, backtick, or Tailwind variant colon (`:`). Variant prefixes stay attached: `sm:fade-t` → `sm:fade-top`, `hover:fade-l` → `hover:fade-start`. Never replace a *prefix* of a longer class — `fade-r` must not match inside `fade-range-*` (or the new `fade-travel-*`). The Step 2 family rules are self-protecting because each prefix ends in `-`. + +--- + +## Step 1 — Judgment calls (do first; never blind-replace) + +### `fade-l` / `fade-r` were *physical*; `fade-start` / `fade-end` are *direction-aware* ⚠️ + +This is the one genuinely unsafe transform. In LTR the mapping is exact and automatable; inside `dir="rtl"` it **inverts** — `fade-start` resolves to the *right* edge — so RTL cases need human review, not a blind swap. + +| Context | `fade-l` → | `fade-r` → | +| --- | --- | --- | +| LTR container (the default) | `fade-start` ✅ exact | `fade-end` ✅ exact | +| RTL, author meant the physical **left** edge | `fade-end` ⚠️ review | — | +| RTL, author meant the physical **right** edge | — | `fade-start` ⚠️ review | + +**Flag every `fade-l` / `fade-r` inside an RTL subtree for human review.** There is no physical-side escape hatch by design (see [Why Plain Directions](#why-plain-directions)). + +### `fade-x` — same name, new behavior + +`fade-x` is **not renamed**, but it now fades the inline start + end edges (direction-aware) rather than fixed physical left + right. No edit; just note the behavior change where RTL applies. + +--- + +## Step 2 — Mechanical renames (safe to automate) + +### Bare direction & state classes — match the whole token + +| Old | New | +| --- | --- | +| `fade-xy` | `fade` | +| `fade-t` | `fade-top` | +| `fade-b` | `fade-bottom` | +| `fade-l` | `fade-start` _(Step 1 caveat)_ | +| `fade-r` | `fade-end` _(Step 1 caveat)_ | +| `fade-static` | `fade-always` | +| `fade-y` | _unchanged_ | +| `fade-x` | _unchanged name (Step 1)_ | + +### Family classes — replace the prefix; the suffix carries through + +The suffix may be a named scale (`-md`), a bare integer (`-2`), or an arbitrary value (`-[80px]`) — replacing only the prefix preserves all three. Each prefix ends in `-`, making the rule self-protecting (`fade-clear-t-md` → `fade-clear-top-md` no longer matches `fade-clear-t-`). + +| Old prefix | New prefix | +| --- | --- | +| `fade-size-t-` | `fade-size-top-` | +| `fade-size-b-` | `fade-size-bottom-` | +| `fade-size-l-` | `fade-size-start-` | +| `fade-size-r-` | `fade-size-end-` | +| `fade-range-` | `fade-travel-` | +| `fade-clear-t-` | `fade-clear-top-` | +| `fade-clear-b-` | `fade-clear-bottom-` | +| `fade-clear-l-` | `fade-clear-start-` | +| `fade-clear-r-` | `fade-clear-end-` | +| `fade-clear-xy-` | `fade-clear-` | +| `fade-size-x-` · `fade-size-y-` · `fade-clear-x-` · `fade-clear-y-` | _unchanged_ | + +`range` → `travel` controls how quickly the soft band eases open as you scroll (the masked edge itself is now covered almost immediately — see the [changelog](./CHANGELOG.md)). `fade-travel-*` is **global only** — no per-edge travel utilities. The default travel is now `sm` (was `md`), so a surface that relied on the implicit default gets a slightly snappier band open — purely cosmetic, no action needed. + +--- + +## Step 3 — Theme tokens & inline custom properties + +| Old | New | +| --- | --- | +| `--fade-range-*` (e.g. `--fade-range-md`) | `--fade-travel-*` | +| `--fade-clear-t` | `--fade-clear-top` | +| `--fade-clear-b` | `--fade-clear-bottom` | +| `--fade-clear-l` | `--fade-clear-start` | +| `--fade-clear-r` | `--fade-clear-end` | +| `--fade-clear-xy` | `--fade-clear` | + +--- + +## Step 4 — New utilities (no old equivalent) + +`fade-none`, `fade-none-x`, `fade-none-y`, `fade-always-x`, `fade-always-y` are new. They change only the fade *amount*, so pair them with a direction utility: + +```html +
+``` + +No migration action — listed so they aren't flagged as unrecognized. + +--- + +## Step 5 — Pin the CDN (prebuilt-CSS users only) + +If the project links the prebuilt CSS, pin the version so this breaking rename can't arrive unannounced: + +```html + +``` + +An unversioned URL (`https://unpkg.com/tw-fade/dist/tw-fade.css`) tracks latest and will receive future breaking renames. Projects that build from the Tailwind source path pick up the new names on the next build — no pin needed. + +--- + +## Step 6 — Verify + +Run from the project root. A complete migration prints **nothing** (the pattern matches every removed class/variable shape with token boundaries, so it won't false-positive on the new names): + +```bash +rg -nP '(? +
``` -The fade reveals as you scroll into content and recedes as you reach the ends. - ---- +The fade appears only when there is content beyond that edge, then recedes again when you reach the start or end. ## Why -A static edge scrim (a gradient overlay pinned to the top of a scroll area) is always on. It dims content even when nothing is scrolled out of view, and it sits *in front of* your content as a separate element. - -`tw-fade` fixes both: - -- **Scroll-gated.** The fade appears only when content has scrolled past the edge, and retracts when you reach the start or end. It never dims content you can already see in full. -- **Masks the container itself.** The effect is a `mask-image` on the scroll container: no extra DOM, no stacking-context juggling. It reveals whatever surface sits behind the element instead of painting a fixed color on top. - -Add `fade-t`, `fade-b`, `fade-x`, `fade-y`, or `fade-xy` to a scrollable element and you're done. - -### Highlights - -- **No runtime JavaScript.** CSS scroll-driven animations (`animation-timeline: scroll()`) drive the reveal; the fade itself is a composited `mask-image`. -- **Composable per edge.** Fade one edge, an axis, or all four. The utilities compose (`fade-t fade-r` works) instead of overwriting each other, and nested fades stay isolated. -- **Tunable.** Set band thickness, reveal distance, and clearance zones for sticky headers and footers through utilities or theme tokens, with arbitrary values on the Tailwind source path. -- **Smooth curve.** The fade follows an eased sigmoid (S-curve) alpha ramp instead of a flat linear gradient, so opacity changes faster through the middle and eases at the ends. -- **Graceful fallback.** On engines without scroll-driven animations, the effect degrades to a permanent static fade via `@supports`, so content stays readable rather than stranded behind a half-applied mask. - ---- - -## Requirements - -| Use it as… | You need | -| --- | --- | -| Tailwind v4 source utilities (full JIT, arbitrary values) | Tailwind CSS **v4.0.0+** | -| Prebuilt framework-free stylesheet (`` / CDN, fixed utility set) | **nothing** (no Tailwind, no build step) | +A static gradient overlay is always on. It dims content even when nothing is hidden beyond the edge, and it sits in front of your content as another layer. -The package ships a precompiled, self-contained `dist/tw-fade.css` with only the `fade-*` utilities and their `@property` foundations. No Tailwind Preflight, no reset, no Tailwind core `--tw-*` variables. +`tw-fade` uses a CSS mask on the scroll container itself: ---- +- **Scroll-aware.** The fade is gated by the element's own scroll position. +- **No extra DOM.** The mask lives on the scroll container. +- **Surface-neutral.** The mask reveals whatever is behind the element instead of painting a fake background color. +- **Composable.** Direction, size, travel distance, and clear zones are separate utilities. +- **Graceful fallback.** Browsers without scroll-driven animations get a static fade instead of a broken mask. ## Install -### Tailwind v4 (CSS-first source path) +### Tailwind v4 source path -Use this if you already build with Tailwind v4. You get the full JIT, including arbitrary values like `fade-size-[2rem]`. +Use this when your app already builds with Tailwind v4. This path supports arbitrary values like `fade-size-[2rem]`. ```bash npm install tw-fade ``` -In your Tailwind entry CSS: - ```css @import "tailwindcss"; @import "tw-fade"; ``` -The bare `@import "tw-fade"` resolves to the package's `src/tw-fade.css`, authored for v4's CSS-first pipeline and compiled by *your* Tailwind build. - -### Prebuilt drop-in (no Tailwind required) +### Prebuilt CSS -Use the precompiled stylesheet directly in plain HTML, over a CDN, or through a bundler. +Use this for plain HTML, CDN usage, or bundlers that import CSS directly. ```html - + ``` ```js -// or, via a JS/bundler import: import "tw-fade/css"; ``` -The drop-in ships a fixed, enumerated set of utilities: the named scale only, no arbitrary bracket values. See [Source path vs. prebuilt](#source-path-vs-prebuilt). +The prebuilt file includes only the named utility set. Arbitrary bracket values need Tailwind's source path. ---- +If you use an unversioned CDN URL, you receive the latest package and any breaking API changes that come with it. Pin a version when you need stable HTML. ## Usage -`tw-fade` is plain CSS classes. Apply them however your framework sets `class`. +Put the fade utility on the element that actually scrolls. ```html - -
+ +
...
- -
+ +
...
- -
+ +
...
- -
+ +
...
- -
+ +
...
``` -The fade keys off the element's **own** scroll position, so put the utility on the element that scrolls (the one with `overflow-*: auto/scroll`), not a parent. - ---- - -## API reference - -### Direction utilities +## Direction Utilities -Each utility masks one or more physical edges. Vertical edges track the element's vertical scroll; horizontal edges track its horizontal scroll. - -| Utility | Edges masked | +| Utility | Edges | | --- | --- | -| `fade-t` | top | -| `fade-b` | bottom | -| `fade-l` | left | -| `fade-r` | right | +| `fade` | all four edges | | `fade-y` | top + bottom | -| `fade-x` | left + right | -| `fade-xy` | all four | -| `fade-static` | pins the selected fade(s) fully on, always (see below) | +| `fade-top` | top | +| `fade-bottom` | bottom | +| `fade-x` | horizontal start + horizontal end | +| `fade-start` | horizontal start | +| `fade-end` | horizontal end | + +`start` and `end` are **horizontal only** and follow semantic reading direction. In LTR, start is the left edge and end is the right edge; in RTL, start is the right edge and end is the left edge. For the top or bottom edge use `fade-top` / `fade-bottom` — there is no `fade-block-start`. Use the HTML `dir` attribute on the scroll container or an ancestor; a CSS-only `direction: rtl` rule does not trigger RTL routing. -`fade-static` forces the fade on regardless of scroll and disables the scroll-driven reveal. It sets only the fade *amounts*, not which edges are masked, so pair it with a direction utility: `fade-y fade-static` renders a permanent top and bottom fade. (It pins all four edge amounts internally, but only edges selected by a direction utility render a mask.) +Vertical names stay physical (`top` / `bottom`) because the block axis is not affected by text direction. This split — physical vertical, direction-aware horizontal — mirrors how Tailwind itself ships physical `top`/`bottom` insets alongside logical `ps`/`pe`. See [Why plain directions](./MIGRATING.md#why-plain-directions) for the naming rationale. -### Size: fade band thickness +## Size -Controls how thick the faded band is. Accepts the [named scale](#named-scale), a `[length]`, or a `[percentage]`. +Size controls how thick the fade band is. | Utility | Affects | | --- | --- | -| `fade-size-*` | global band thickness (all edges) | -| `fade-size-t-*` / `-b-*` / `-l-*` / `-r-*` | a single edge | -| `fade-size-y-*` | both vertical edges | -| `fade-size-x-*` | both horizontal edges | +| `fade-size-*` | all edges | +| `fade-size-y-*` | top + bottom | +| `fade-size-x-*` | start + end | +| `fade-size-top-*` | top | +| `fade-size-bottom-*` | bottom | +| `fade-size-start-*` | horizontal start | +| `fade-size-end-*` | horizontal end | ```html -
+
...
``` -Resolution runs **edge → axis → global → default** (`fade-size-md`, i.e. `3rem`): a per-edge value beats an axis value, which beats the global value, which falls back to the default. See [Resolution precedence](#resolution-precedence). +Resolution is edge, then axis, then global, then the default. For example, top uses `fade-size-top-*` first, then `fade-size-y-*`, then `fade-size-*`. -### Range: scroll reveal distance +The default size is capped at `min(12%, 3rem)`, so small scroll areas do not get swallowed by the fade. Named sizes derive from Tailwind's spacing unit, `--spacing`, with `0.25rem` as the fallback. -Controls the scroll distance over which a fade reveals (as you scroll in) or retracts (as you reach the end). Accepts the named scale, a `[length]`, or a `[percentage]`. +| Step | Value | Step | Value | +| --- | --- | --- | --- | +| `xs` | `1.5rem` | `xl` | `5rem` | +| `sm` | `2rem` | `2xl` | `6rem` | +| `md` | `3rem` | `3xl` | `8rem` | +| `lg` | `4rem` | `4xl` | `10rem` | -| Utility | Affects | -| --- | --- | -| `fade-range-*` | reveal distance (default `fade-range-md`, i.e. `3rem`) | +You can override the tokens: -A short range snaps the fade in; a long range eases it over more scroll travel. +```css +@theme { + --fade-size-md: 2.5rem; +} +``` + +On the Tailwind source path, size accepts named values, lengths, and percentages: -### Clear: clearance zone before the fade +```html +
...
+``` -An opaque, unfaded band held at the edge before the fade ramp begins. Use it behind a sticky header or footer so the pinned UI stays fully visible while content fades beneath it. +The prebuilt CSS includes the named sizes only. + +## Travel Distance + +Travel distance controls how far you scroll before the soft band eases open to its full width. | Utility | Affects | | --- | --- | -| `fade-clear-t-*` / `-b-*` / `-l-*` / `-r-*` | a single edge | -| `fade-clear-y-*` | both vertical edges | -| `fade-clear-x-*` | both horizontal edges | -| `fade-clear-xy-*` | all four edges | - -The `*` accepts the named scale, a bare integer (`N` = `spacing × N`, e.g. `fade-clear-t-4` = `1rem`), a `[length]`, or a `[percentage]`. Default clearance is `0`. +| `fade-travel-*` | all selected edges | ```html - -
-
- … -
+
...
+
...
``` -> On macOS, rubber-band overscroll can briefly reveal a gap between a sticky element and the clear zone, because the mask stays fixed to the scroll container while the sticky element rubber-bands with the content. If that matters, fade only the opposite edge (e.g. `fade-b` under a top-pinned header). - -#### Dynamic clear zones (`-var`) +A smaller travel distance opens the band faster; a larger one eases it over more scroll. Either way the **edge is masked almost immediately** — the band's transparency saturates within `travel ÷ 8` of scroll, so content is never hard-clipped at the scroll edge while the band is still widening. The travel is purely the cosmetic open-speed; it is safe at any size. -Every clear utility has a `-var` form that reads the clearance from a CSS custom property you set, so you can drive it from JS or layout-dependent values: +The named travel scale is the same as the size scale (default `sm`) and can be overridden with `--fade-travel-{step}`. Arbitrary travel values are source-path only. To change how fast the edge itself goes transparent, set `--tw-fade-onset` (default `8`; higher = snappier). -`fade-clear-t-var`, `fade-clear-b-var`, `fade-clear-l-var`, `fade-clear-r-var`, `fade-clear-y-var`, `fade-clear-x-var`, `fade-clear-xy-var`. +## Clear Zones -Each resolves its property along an **edge → axis → xy → `0px`** chain. The public custom properties you can set: +Clear zones keep a fully opaque strip before the fade starts. Use them for sticky headers, sticky footers, or fixed controls inside the scroll container. -| Property | Read by | +| Utility | Affects | | --- | --- | -| `--fade-clear-t` / `-b` / `-l` / `-r` | the matching per-edge `-var` | -| `--fade-clear-y` / `-x` | the matching axis `-var` (and per-edge as a fallback) | -| `--fade-clear-xy` | all `-var` utilities (final fallback before `0px`) | +| `fade-clear-*` | all edges | +| `fade-clear-y-*` | top + bottom | +| `fade-clear-x-*` | start + end | +| `fade-clear-top-*` | top | +| `fade-clear-bottom-*` | bottom | +| `fade-clear-start-*` | horizontal start | +| `fade-clear-end-*` | horizontal end | ```html -
+
+
...
+ ... +
``` -```js -// e.g. keep clearance in sync with a measured sticky header -el.style.setProperty("--fade-clear-t", header.offsetHeight + "px"); -``` +Clear utilities accept named values, lengths, percentages, and bare integers on the Tailwind source path. A bare integer maps to `--spacing * N`, so `fade-clear-top-4` is `1rem` with the default spacing unit. -> The `-var` family is the only one with a dynamic form. `fade-range` has no `-var` variant. +The prebuilt CSS includes named clear values and `-var` forms, not arbitrary values or integer forms. -### Named scale +### Dynamic Clear Zones -Used by every `fade-size-*`, `fade-range-*`, and named `fade-clear-*` utility. Values derive from Tailwind's spacing unit (`--spacing`, default `0.25rem`). +Use `-var` when the clear zone depends on runtime layout: -| Step | Value | Step | Value | -| --- | --- | --- | --- | -| `xs` | `1.5rem` | `xl` | `5rem` | -| `sm` | `2rem` | `2xl` | `6rem` | -| `md` | `3rem` | `3xl` | `8rem` | -| `lg` | `4rem` | `4xl` | `10rem` | +```html +
+ ... +
+``` -Read or override them as theme tokens: `--fade-size-{step}`, `--fade-range-{step}`, `--fade-clear-{step}`. +Available forms: -### Resolution precedence +```txt +fade-clear-var +fade-clear-y-var +fade-clear-x-var +fade-clear-top-var +fade-clear-bottom-var +fade-clear-start-var +fade-clear-end-var +``` -For each edge, the band **size** resolves from most specific to least: +The fallback chain is edge, then axis, then global, then `0px`. -``` -edge-specific → axis → global → default -fade-size-t-* → fade-size-y-* → fade-size-* → 3rem (fade-size-md) +```css +--fade-clear-top: 56px; +--fade-clear-y: 24px; +--fade-clear: 0px; ``` -The top edge takes its top value if set, else the vertical-axis value, else the global value, else the default `3rem`. Horizontal edges follow the same chain through the x-axis value. +## Force Or Disable A Fade -Dynamic **clearance** (`-var`) resolves `edge → axis → xy → 0px`. +These utilities change only the active fade amount. They do not select edges on their own, so pair them with a direction utility. ---- +| Utility | Effect | +| --- | --- | +| `fade-none` | disables all selected fades | +| `fade-none-y` | disables selected vertical fades | +| `fade-none-x` | disables selected horizontal fades | +| `fade-always` | pins all selected fades fully on | +| `fade-always-y` | pins selected vertical fades fully on | +| `fade-always-x` | pins selected horizontal fades fully on | + +```html +
...
+
...
+``` -## Fading the whole page +## Fading The Whole Page -To dissolve content into the top and bottom of the **viewport** as the page scrolls, fade the element that scrolls. Make `` the scroll container over a surface on ``: +Fade the element that scrolls. For a full-page fade, make `` the scroll container and keep the surface behind it on ``. ```html - - - … + ... ``` -Two requirements people miss: - -1. **`` must be the scroll container.** `scroll(self y)` tracks only the element's own scrollport. Give `` `height: 100%` and `overflow-y: auto`, and set `overflow: hidden` on ``. Otherwise the browser propagates the body's overflow up to the viewport, `` never becomes a scroller, and the fade does nothing. -2. **Put a surface behind the mask.** A mask also masks the element's own background, so something has to sit behind `` to reveal. Keep the surface color on `` (or a fixed backdrop) and make `` transparent. - -Masking `` also establishes a stacking context on it, which can change the `z-index` layering of fixed and absolute descendants. - ---- - -## Troubleshooting +Two details matter: -- **Is the class on the scrollable element?** The fade has to live on the element that overflows and scrolls, not a parent. See [Usage](#usage). -- **Can it scroll?** With no overflow on an axis, that axis's timeline stays inactive and the amount holds at its `0` base, so no fade. This is intentional: don't fade what you can't scroll. -- **Is there a contrasting surface behind it?** A mask reveals whatever sits behind the element. With nothing there, or no contrast, there's nothing to fade into. -- **RTL plus horizontal?** See [Writing direction](#writing-direction-rtl). +1. `` must have a fixed height and `overflow-y-auto`, otherwise the viewport scrolls instead. +2. The scroll container should be transparent if you want the mask to reveal the page surface behind it. ---- +## Source Path Vs. Prebuilt -## How it works +| | `@import "tw-fade"` | `tw-fade/css` | +| --- | --- | --- | +| Needs Tailwind v4 | yes | no | +| Direction utilities | yes | yes | +| Named size/travel/clear utilities | yes | yes | +| `fade-clear-*-var` | yes | yes | +| Arbitrary values like `fade-size-[6rem]` | yes | no | +| Integer clear values like `fade-clear-top-14` | yes | no | -The element gets a `mask-image` built from four comma-separated layers, one per physical edge, combined with `mask-composite: intersect`: +The prebuilt file is generated from an explicit safelist. It does not include Tailwind Preflight, core Tailwind utilities, arbitrary values, or integer `fade-clear-*` classes. -```css -mask-image: - /* top */ var(--tw-fade-mask-t, linear-gradient(#000, #000)), - /* bottom */ var(--tw-fade-mask-b, linear-gradient(#000, #000)), - /* left */ var(--tw-fade-mask-l, linear-gradient(#000, #000)), - /* right */ var(--tw-fade-mask-r, linear-gradient(#000, #000)); -mask-composite: intersect; -``` +## Exports -When an edge is active, its layer holds a real `linear-gradient`. Otherwise it falls back to the opaque identity gradient `linear-gradient(#000, #000)`, which masks nothing. `intersect` is mandatory: the CSS default `add` (union) lets one opaque layer cancel another's transparency and erase the fade. Each direction utility sets only its own edge's layer, so classes compose cleanly. - -**The amount drives both alpha and length.** Each active edge carries a numeric amount from `0` to `1` that controls the gradient's opacity (at `0`, every stop is fully opaque, so the layer is a no-op) and the band's length (at `0`, the band collapses to zero). At `1` you get the full fade over the full band thickness. +| Specifier | Resolves to | +| --- | --- | +| `tw-fade` | `./src/tw-fade.css` | +| `tw-fade/css` | `./dist/tw-fade.css` | +| `tw-fade/dist/tw-fade.css` | `./dist/tw-fade.css` | -**The curve.** After the clearance zone, the active gradient is a 13-stop ramp. The alpha stops follow an eased, symmetric sigmoid sequence while their positions stay evenly spaced across the band. Eased opacity against linear positions yields a smooth S-curve: denser opacity change near the middle, gentler at the ends. +`tailwindcss >=4.0.0` is an optional peer dependency. You need it for the source path, but not for the prebuilt CSS. -**Scroll gating.** Inside `@supports (animation-timeline: scroll())`, each utility binds the four edge amounts to scroll-driven animations: vertical edges to `scroll(self y)`, horizontal edges to `scroll(self x)`. Leading edges (top, left) reveal `0 → 1` over the first `fade-range` of scroll; trailing edges (bottom, right) retract `1 → 0` over the last. When an axis can't scroll, its amount holds at the `0` base, so no fade. That is the "don't fade what you can't scroll past" guard. +## How It Works -**Fallback.** Inside `@supports not (animation-timeline: scroll())`, all amounts pin to `1`, so every selected edge renders its full static fade instead of disappearing. +Each faded element gets a four-layer `mask-image`, one layer per physical edge. Inactive layers fall back to an opaque identity mask. Active layers use a 13-stop eased gradient. -> **Public vs. internal surface.** The supported API is the `fade-*` utilities, the `--fade-*` theme tokens, and the `--fade-clear-*` dynamic vars above. The `--tw-fade-*` custom properties in this section are internal implementation details. They register with `inherits: false` so nested fades stay isolated, and they are not a surface to target directly. +The public API uses plain direction names, but the internal engine still keeps four physical edge amounts. Those internal properties are typed numbers, do not inherit, and are driven by scroll animations. Keeping the engine physical makes the mask predictable; the public `start` and `end` utilities route to the correct physical side for LTR or RTL in browsers that support the `:dir()` selector (see [Browser Support](#browser-support)). ---- +Inside browsers with scroll-driven animation support: -## Writing direction (RTL) +- vertical fades use `scroll(self y)`; +- horizontal fades use `scroll(self inline)`; +- leading edges reveal from `0` to `1` near the start of scroll; +- trailing edges retract from `1` to `0` near the end of scroll. -Behavior keys off **physical** axes, not inline text direction. +If an axis cannot scroll, that axis stays at `0`, so the fade does not show. This is intentional. -- **Vertical fades (`fade-t`, `fade-b`, `fade-y`) are RTL-safe.** The vertical axis and the top and bottom gradients stay identical regardless of text direction. -- **Horizontal fades assume LTR.** `fade-l` acts as the leading edge (reveals as you scroll in) and `fade-r` as the trailing edge (retracts at the end). Because the timeline uses the physical `scroll(self x)` axis, an RTL context flips the leading and trailing semantics relative to reading order. For an order-neutral horizontal scroller, set `dir="ltr"` on the scroll container. +In browsers without scroll-driven animations, selected fades pin fully on as a static fallback. ---- +The supported public surface is the `fade-*` utilities, the public `--fade-*` tokens described above, and `--tw-fade-onset` (the edge-speed knob documented under [Travel Distance](#travel-distance)). Treat the rest of the `--tw-fade-*` namespace as internal implementation detail. -## Accessibility +## RTL -`tw-fade` is a visual mask only. It animates the opacity and length of an edge gradient, never the position of content, and it adds no scrolling motion beyond the scroll you already perform. Like any edge scrim, it lowers the contrast of content right at the masked edges, so keep band sizes modest where edge text must stay legible, and use `fade-clear-*` to hold critical content fully opaque. +Horizontal fades are direction-aware: ---- +```html +
...
+``` -## Source path vs. prebuilt +In that example, `fade-start` selects the right edge because the scroll container is RTL. `fade-end` selects the left edge. `fade-x` selects both. -| | `@import "tw-fade"` (source) | `tw-fade/css` (prebuilt) | -| --- | --- | --- | -| Requires Tailwind v4 | yes (compiles through your build) | no | -| Direction / `fade-static` | yes | yes | -| Named-scale utilities (`xs`–`4xl`) | yes | yes | -| Per-edge & axis size/clear utilities | yes | yes | -| `fade-clear-*-var` dynamic forms | yes | yes | -| **Arbitrary values** (`fade-size-[6rem]`, `fade-range-[80px]`, `fade-clear-t-[56px]`) | **yes** | **no** | +RTL routing follows semantic direction through `dir`, not a CSS-only `direction: rtl` declaration. It relies on the `:dir()` selector, so on the narrow band of Chromium that supports scroll-driven animations but not `:dir()` (Chrome 115–119), RTL scrollers fall back to LTR edge mapping — see [Browser Support](#browser-support). LTR is unaffected. -The prebuilt drop-in compiles only `tailwindcss/utilities.css`, so it carries no Preflight, no reset, and no Tailwind core `--tw-*` defaults. Just the enumerated `fade-*` utilities and their `@property` registrations. Arbitrary bracket values need Tailwind's JIT and exist only on the source path. +Vertical fades are unchanged by text direction. -### Exports - -| Specifier | Resolves to | -| --- | --- | -| `tw-fade` (`.`) | `./src/tw-fade.css` (v4 CSS-first source) | -| `tw-fade/css` | `./dist/tw-fade.css` (prebuilt drop-in) | -| `tw-fade/dist/tw-fade.css` | `./dist/tw-fade.css` (explicit path) | +## Accessibility -`tailwindcss >=4.0.0` is an **optional** peer dependency. Consumers using only the prebuilt drop-in don't need it; the source path still needs Tailwind v4 to compile. +`tw-fade` is visual only. It does not move content, add scroll behavior, or change focus order. Like any edge fade, it lowers contrast near the masked edge, so keep band sizes modest and use clear zones for sticky controls or critical text. ---- +## Browser Support -## Browser support +Three CSS features carry different support floors: -Two CSS features carry different support floors: +- **CSS masking** (`mask-image`, `mask-composite`). Interoperable across current evergreen browsers. +- **Scroll-driven animations** (`animation-timeline: scroll()`). Shipped by default in every major engine except Firefox release. +- **The `:dir()` selector**, used to route `start` / `end` to the correct physical edge under RTL. -- **CSS masking** (`mask-image`, `mask-composite`, unprefixed). Interoperable across every current evergreen browser. See [caniuse: mask-image](https://caniuse.com/mdn-css_properties_mask-image). -- **Scroll-driven animations** (`animation-timeline: scroll()`). Shipped by default in every major engine except Firefox. See [caniuse: scroll-driven animations](https://caniuse.com/mdn-css_properties_animation-timeline_scroll). +| Engine | Masking | Scroll-driven animation | `:dir()` | Result | +| --- | --- | --- | --- | --- | +| Chrome / Edge | 120+ | 115+ | 120+ | Full scroll-gated, direction-aware fade | +| Safari (WebKit) | 15.4+ | 26.0+ | 16.4+ | Full on 26+; static fade fallback below | +| Firefox (release) | 53+ | not by default | 49+ | Static always-on fade | -| Engine | Masking | Scroll-driven animation | Result | -| --- | --- | --- | --- | -| Chrome / Edge | 120+ | 115+ | Full scroll-gated fade | -| Safari (WebKit) | 15.4+ / iOS 15.4+ | **26.0+** | Full on 26+; static fade fallback below | -| Firefox (release/beta) | 53+ | **not by default** | Static always-on fade | -| Opera | 106+ | 101+ | Full scroll-gated fade | -| Samsung Internet | 25+ | 23+ | Full scroll-gated fade | +**Scroll-driven fallback.** Browsers without scroll-driven animations still render selected fades, but as always-on static fades. Safari 17.x / 18.x and Firefox release get this fallback (Firefox keeps scroll-driven animations behind a flag, default-on only in Nightly). -**Firefox.** Scroll-driven animations are implemented but sit behind the `layout.css.scroll-driven-animations.enabled` flag, default-on only in Nightly (since 136) and default-off in Release, Beta, and Developer Edition. Firefox release users get the static `@supports` fallback (the fade renders fully on, always). Masking works regardless (Firefox 53+). +**RTL routing floor.** Direction-aware `start` / `end` routing additionally needs `:dir()`. On the narrow band of Chromium that has scroll-driven animations but not `:dir()` (Chrome 115–119), RTL scrollers fall back to LTR physical-edge mapping — the fade still renders, but `start` / `end` map to the wrong side. LTR is unaffected, and that band is effectively gone on auto-updating Chromium. -**Safari.** Scroll-driven animations shipped by default in Safari 26.0; 17.x and 18.x get the static fallback. Older WebKit may also need `-webkit-mask-*`, so run the prebuilt CSS through Autoprefixer if you target it. +**Older WebKit** may also need `-webkit-mask-*`; run the prebuilt CSS through Autoprefixer if you target it. -The practical masking floor is Chrome/Edge 120, the last major engine to drop the `-webkit-` prefix requirement; Safari and Firefox were already ahead. In every case the `@supports` fallback degrades the effect to a permanent static fade rather than breaking. +## Migrating ---- +Upgrading from `0.6.x`? [MIGRATING.md](./MIGRATING.md) is written as a step-by-step +procedure you can hand straight to a coding agent ("upgrade this project to tw-fade +0.7.0 using MIGRATING.md") — ordered renames, the physical→logical RTL caveats to +review by hand, and a one-line grep to verify nothing was missed. It reads fine for +humans too. ## Development ```bash -npm test # compiled-CSS unit tests, no browser required -npm run build # regenerate dist/tw-fade.css from src/tw-fade.css +npm test +npm run build +npm run build:demo +npm run verify ``` - -`dist/tw-fade.css` is generated. Don't edit it by hand. - ---- - -## License - -MIT © Pete Petrash diff --git a/build/verify-demo-runtime.mjs b/build/verify-demo-runtime.mjs index 760f527..a7b71a6 100644 --- a/build/verify-demo-runtime.mjs +++ b/build/verify-demo-runtime.mjs @@ -154,7 +154,7 @@ async function runInteractions(page, label) { const depthAfter = await page.locator('[data-fade-depth-value]').textContent() assert(depthBefore !== depthAfter, `${label}: fade-depth slider updates from keyboard`, `${depthBefore} -> ${depthAfter}`) - const topEdge = page.locator('[data-fade-edge-toggle="t"]') + const topEdge = page.locator('[data-fade-edge-toggle="top"]') await topEdge.focus() await page.keyboard.press('Space') await page.waitForTimeout(160) @@ -249,7 +249,7 @@ async function smokePage(browser, options) { ) } assert(state.verticalRows >= 5, `${label}: vertical list rendered`, String(state.verticalRows)) - assert(state.advancedLabel === 'fade-y fade-size-2xl fade-range-2xl', `${label}: advanced controls initialize class readout`, state.advancedLabel) + assert(state.advancedLabel === 'fade-y fade-size-2xl fade-travel-2xl', `${label}: advanced controls initialize class readout`, state.advancedLabel) assert(state.edgePressed === 'true,false,true,false', `${label}: top and bottom edge toggles initialize on`, state.edgePressed) assert(state.easedLine && state.scrollAwareThumb && state.composableTokens >= 3, `${label}: key animated graphics rendered`) assert(state.documentOverflowX <= 1, `${label}: no document-level horizontal overflow`, String(state.documentOverflowX)) @@ -267,32 +267,53 @@ async function smokePage(browser, options) { assert(state.waveMode === 'svg', `${label}: procedural SVG wave field activates`, state.waveMode) assert(state.waveBeforeDisplay === 'none', `${label}: procedural wave hides static fallback`, state.waveBeforeDisplay) assert(state.waveFieldDisplay === 'block', `${label}: SVG wave field renders`, state.waveFieldDisplay) - assert(state.wavePathD.startsWith('M64 0C124'), `${label}: SVG wave starts at top geometry`, state.wavePathD.slice(0, 20)) + assert(state.wavePathD.startsWith('M64 0C75.781'), `${label}: SVG wave starts at top geometry`, state.wavePathD.slice(0, 24)) assert(state.wavePatternHeight === '1629', `${label}: SVG wave pattern starts at top wavelength`, state.wavePatternHeight) } assert(/transform/.test(state.movingWillChange.railCard), `${label}: rail cards are composited`, state.movingWillChange.railCard) assert(/transform/.test(state.movingWillChange.easedLine), `${label}: eased line is composited`, state.movingWillChange.easedLine) assert(/opacity/.test(state.movingWillChange.token), `${label}: composable tokens expose compositor hints`, state.movingWillChange.token) - await page.evaluate(() => { - document.body.scrollTo({ - top: Math.min(900, document.body.scrollHeight - document.body.clientHeight), - left: 0, - behavior: 'instant', - }) - }) - await page.waitForTimeout(240) - assert(await page.evaluate(() => Boolean(document.body)), `${label}: page survives scroll-linked animation probe`) + // Drive the scroll-linked wave field and confirm it morphs. Set scrollTop + // directly (the most portable scroll primitive) and dispatch the `scroll` + // event the field listens for: headless Linux WebKit does not always emit a + // native scroll event for a *programmatic* position change on an overflow + // body the way macOS WebKit does, which would otherwise freeze the wave and + // fail this check only in CI. Then poll for the morph (rAF-driven) instead + // of betting on a single fixed delay. + const waveAfterScroll = await page.evaluate( + async ({ initialD, initialHeight, reduced }) => { + const body = document.body + body.scrollTop = Math.min(900, body.scrollHeight - body.clientHeight) + body.dispatchEvent(new Event('scroll')) + const sample = () => ({ + d: document.querySelector('[data-demo-wave-path]')?.getAttribute('d') ?? '', + height: document.querySelector('[data-demo-wave-pattern]')?.getAttribute('height') ?? '', + bodyAlive: Boolean(document.body), + }) + if (reduced) return sample() + const deadline = performance.now() + 3000 + let current = sample() + while ( + performance.now() < deadline && + current.height === initialHeight && + current.d === initialD + ) { + await new Promise((resolve) => requestAnimationFrame(resolve)) + current = sample() + } + return current + }, + { initialD: state.wavePathD, initialHeight: state.wavePatternHeight, reduced: reducedMotion }, + ) + assert(waveAfterScroll.bodyAlive, `${label}: page survives scroll-linked animation probe`) if (!reducedMotion) { - const waveAfterScroll = await page.evaluate(() => ({ - d: document.querySelector('[data-demo-wave-path]')?.getAttribute('d') ?? '', - height: document.querySelector('[data-demo-wave-pattern]')?.getAttribute('height') ?? '', - })) assert(waveAfterScroll.d && waveAfterScroll.d !== state.wavePathD, `${label}: wave path geometry changes with scroll`, waveAfterScroll.d.slice(0, 32)) assert(waveAfterScroll.height !== state.wavePatternHeight, `${label}: wave wavelength changes with scroll`, `${state.wavePatternHeight} -> ${waveAfterScroll.height}`) } await page.evaluate(() => { - document.body.scrollTo({ top: 0, left: 0, behavior: 'instant' }) + document.body.scrollTop = 0 + document.body.dispatchEvent(new Event('scroll')) }) await page.waitForTimeout(120) diff --git a/build/verify-dist.mjs b/build/verify-dist.mjs index 2f57bb5..29a86f5 100644 --- a/build/verify-dist.mjs +++ b/build/verify-dist.mjs @@ -23,14 +23,14 @@ const html = `
-
+
-
+
@@ -54,8 +54,8 @@ async function probe(sel, top) { maxScroll: el.scrollHeight - el.clientHeight, t: num('--tw-fade-t'), b: num('--tw-fade-b'), - range: cs.getPropertyValue('--tw-fade-range').trim(), - rangeActive: cs.getPropertyValue('--tw-fade-range-active').trim(), + travel: cs.getPropertyValue('--tw-fade-travel').trim(), + travelActive: cs.getPropertyValue('--tw-fade-travel-active').trim(), animationRangeStart: cs.animationRangeStart, animationRangeEnd: cs.animationRangeEnd, maskComposite: cs.maskComposite, @@ -94,7 +94,7 @@ const r = { top: await probe('#tall', 0), mid: await probe('#tall', Math.round(tallMax / 2)), bottom: await probe('#tall', tallMax), - range: await probe('#range', 0), + travel: await probe('#travel', 0), short: await probe('#short', 0), combo: await probeBoth('#combo', 100, 100), } @@ -107,10 +107,10 @@ const checks = [ ['dist: mid → both fades (t≈1,b≈1)', approx(r.mid.t, 1) && approx(r.mid.b, 1)], ['dist: bottom → full top fade (t≈1)', approx(r.bottom.t, 1)], ['dist: bottom → no bottom fade (b≈0)', approx(r.bottom.b, 0)], - ['dist: fade-range-2xl resolves into the scroll animation range', /^96px, 100%, 96px, 100%$/.test(r.range.animationRangeEnd)], + ['dist: fade-travel-2xl resolves into the selected vertical animation range', /^96px, 100%, normal, normal$/.test(r.travel.animationRangeEnd)], ['dist: not scrollable → no fade (t≈0,b≈0)', approx(r.short.t, 0) && approx(r.short.b, 0)], - ['dist: mixed fade-t fade-r composes (t≈1,r≈1)', approx(r.combo.t, 1) && approx(r.combo.r, 1)], - ['dist: mixed fade-t fade-r keeps four reveal animations', r.combo.animationNames.length === 4], + ['dist: mixed fade-top fade-end composes (t≈1,r≈1)', approx(r.combo.t, 1) && approx(r.combo.r, 1)], + ['dist: mixed fade-top fade-end keeps four travel animations', r.combo.animationNames.length === 4], ['dist: mask-composite intersect', /intersect/.test(r.top.maskComposite)], ['dist: mask-image has 4 layers', (r.top.maskImage.match(/gradient/g) || []).length >= 1], ] diff --git a/build/verify-fade.mjs b/build/verify-fade.mjs index 6e6d013..53e4fa6 100644 --- a/build/verify-fade.mjs +++ b/build/verify-fade.mjs @@ -11,12 +11,12 @@ * 2. horizontal (fade-x), default size — left & right melt to background * 3. an overridden size (inline `--tw-fade-size`, exactly what `fade-size-[120px]` * JIT-emits) — the fade band SCALES to the larger size, not the default - * 4. a scoped edge size (`fade-size-b-lg`) — the bottom band scales without + * 4. a scoped edge size (`fade-size-bottom-lg`) — the bottom band scales without * changing the top band * 5. nested isolation — a DEFAULT fade under an ancestor that sets a larger * `--tw-fade-size` still renders the DEFAULT band, proving the `@property` * `inherits: false` registration stops size/range leaking down (the cascade fix) - * 6. mixed individual edges (fade-t fade-r) — arbitrary edge utilities compose + * 6. mixed individual edges (fade-top fade-end) — arbitrary edge utilities compose * 7. nested single edge under a faded ancestor — mask layers do not leak down * * Usage: node build/verify-fade.mjs [path-to-css] (defaults to the shipped dist) @@ -192,7 +192,7 @@ const bigCross = crossing(bigLum) const scopedLum = await profileLine({ axis: 'y', innerSelector: '#scoped', - body: `
${blocks('vb', 40)}
`, + body: `
${blocks('vb', 40)}
`, }) const scopedTopCross = crossing(scopedLum) const scopedBottomCross = crossing([...scopedLum].reverse()) @@ -204,22 +204,22 @@ const scopedBottomCross = crossing([...scopedLum].reverse()) const nestLum = await profileLine({ axis: 'y', innerSelector: '#inner', - body: `
${blocks('vb', 40)}
`, + body: `
${blocks('vb', 40)}
`, }) const nestCross = crossing(nestLum) // 6. Mixed individual edges. This catches the cascade failure where one utility's -// animation shorthand used to overwrite the other edge's reveal animation. +// animation shorthand used to overwrite the other edge's travel animation. const combo = await samplePanel({ innerSelector: '#combo', - body: `
`, + body: `
`, }) // 7. A page-level/body-level fade used to leak its mask layer variables into // nested single-edge examples. The child should fade only at the top. const nestedSingle = await samplePanel({ innerSelector: '#nested-single', - body: `
${blocks('vb', 40)}
`, + body: `
${blocks('vb', 40)}
`, }) await browser.close() @@ -257,12 +257,12 @@ const checks = [ `inner≈${nestCross}px (default≈${vCross}px, leaked≈${bigCross}px)`, ], - ['mixed fade-t fade-r: top edge masked toward background', combo.top < 60, `top≈${combo.top.toFixed(0)}`], - ['mixed fade-t fade-r: right edge masked toward background', combo.right < 60, `right≈${combo.right.toFixed(0)}`], - ['mixed fade-t fade-r: middle stays bright', combo.mid > 200, `mid≈${combo.mid.toFixed(0)}`], - ['nested fade-t: top edge masked toward background', nestedSingle.top < 60, `top≈${nestedSingle.top.toFixed(0)}`], - ['nested fade-t: bottom edge remains unmasked', nestedSingle.bottom > 200, `bottom≈${nestedSingle.bottom.toFixed(0)}`], - ['nested fade-t: middle stays bright', nestedSingle.mid > 200, `mid≈${nestedSingle.mid.toFixed(0)}`], + ['mixed fade-top fade-end: top edge masked toward background', combo.top < 60, `top≈${combo.top.toFixed(0)}`], + ['mixed fade-top fade-end: right edge masked toward background', combo.right < 60, `right≈${combo.right.toFixed(0)}`], + ['mixed fade-top fade-end: middle stays bright', combo.mid > 200, `mid≈${combo.mid.toFixed(0)}`], + ['nested fade-top: top edge masked toward background', nestedSingle.top < 60, `top≈${nestedSingle.top.toFixed(0)}`], + ['nested fade-top: bottom edge remains unmasked', nestedSingle.bottom > 200, `bottom≈${nestedSingle.bottom.toFixed(0)}`], + ['nested fade-top: middle stays bright', nestedSingle.mid > 200, `mid≈${nestedSingle.mid.toFixed(0)}`], ] console.log(`CSS: ${path.relative(path.resolve(__dirname, '..'), cssPath)}`) @@ -271,7 +271,7 @@ console.log( `horizontal — left:${hLeft.toFixed(0)} mid:${hMid.toFixed(0)} right:${hRight.toFixed(0)} | ` + `crossings — default:${vCross} big:${bigCross} scoped-top:${scopedTopCross} scoped-bottom:${scopedBottomCross} nested:${nestCross} | ` + `combo — top:${combo.top.toFixed(0)} right:${combo.right.toFixed(0)} mid:${combo.mid.toFixed(0)} | ` + - `nested fade-t — top:${nestedSingle.top.toFixed(0)} bottom:${nestedSingle.bottom.toFixed(0)} mid:${nestedSingle.mid.toFixed(0)}`, + `nested fade-top — top:${nestedSingle.top.toFixed(0)} bottom:${nestedSingle.bottom.toFixed(0)} mid:${nestedSingle.mid.toFixed(0)}`, ) console.log('\n=== FADE PIXEL CHECKS ===') let pass = 0 diff --git a/build/verify-horizontal-rtl.mjs b/build/verify-horizontal-rtl.mjs new file mode 100644 index 0000000..24b4386 --- /dev/null +++ b/build/verify-horizontal-rtl.mjs @@ -0,0 +1,129 @@ +import { chromium } from 'playwright' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const css = fs.readFileSync(path.resolve(__dirname, '../dist/tw-fade.css'), 'utf8') + +const html = ` +
+
+
+
+
+
+
+
+` + +const browser = await chromium.launch() +const page = await browser.newPage({ viewport: { width: 900, height: 700 }, deviceScaleFactor: 1 }) +await page.setContent(html, { waitUntil: 'networkidle' }) + +async function probe(selector, logicalPosition) { + return page.evaluate( + async ([sel, pos]) => { + const el = document.querySelector(sel) + if (!el) throw new Error(`Missing selector: ${sel}`) + const dir = getComputedStyle(el).direction + const max = el.scrollWidth - el.clientWidth + const target = Math.max(0, Math.min(max, pos * max)) + el.scrollLeft = dir === 'rtl' ? -target : target + await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))) + const cs = getComputedStyle(el) + const num = (v) => Number(cs.getPropertyValue(v).trim() || 'NaN') + const prop = (v) => cs.getPropertyValue(v).trim() + return { + selector: sel, + dir, + requested: target, + scrollLeft: el.scrollLeft, + maxScrollLeft: max, + t: num('--tw-fade-t'), + b: num('--tw-fade-b'), + l: num('--tw-fade-l'), + r: num('--tw-fade-r'), + maskL: prop('--tw-fade-mask-l'), + maskR: prop('--tw-fade-mask-r'), + animationName: cs.animationName, + animationTimeline: cs.animationTimeline, + animationRangeStart: cs.animationRangeStart, + animationRangeEnd: cs.animationRangeEnd, + } + }, + [selector, logicalPosition], + ) +} + +const results = { + ltrStart0: await probe('#ltr-start', 0), + ltrStartMid: await probe('#ltr-start', 0.5), + ltrEnd0: await probe('#ltr-end', 0), + ltrEndMax: await probe('#ltr-end', 1), + ltrX0: await probe('#ltr-x', 0), + ltrXMid: await probe('#ltr-x', 0.5), + ltrXMax: await probe('#ltr-x', 1), + rtlStart0: await probe('#rtl-start', 0), + rtlStartMid: await probe('#rtl-start', 0.5), + rtlEnd0: await probe('#rtl-end', 0), + rtlEndMax: await probe('#rtl-end', 1), + rtlX0: await probe('#rtl-x', 0), + rtlXMid: await probe('#rtl-x', 0.5), + rtlXMax: await probe('#rtl-x', 1), + nestedLtrStart0: await probe('#nested-ltr-start', 0), + nestedLtrStartMid: await probe('#nested-ltr-start', 0.5), + short: await probe('#short', 0), +} + +await browser.close() + +const approx = (a, b, tol = 0.08) => Number.isFinite(a) && Math.abs(a - b) <= tol +const hasLeft = (r) => /to right/.test(r.maskL) +const hasRight = (r) => /to left/.test(r.maskR) +const noLeft = (r) => !/to right/.test(r.maskL) +const noRight = (r) => !/to left/.test(r.maskR) + +const checks = [ + ['LTR fade-start maps to physical left only', hasLeft(results.ltrStart0) && noRight(results.ltrStart0)], + ['LTR fade-start starts inactive then reveals', approx(results.ltrStart0.l, 0) && approx(results.ltrStartMid.l, 1)], + ['LTR fade-end maps to physical right only', hasRight(results.ltrEnd0) && noLeft(results.ltrEnd0)], + ['LTR fade-end starts present then retracts at end', approx(results.ltrEnd0.r, 1) && approx(results.ltrEndMax.r, 0)], + ['LTR fade-x uses both horizontal layers', hasLeft(results.ltrXMid) && hasRight(results.ltrXMid)], + ['LTR fade-x is active in middle and inactive at opposite ends', approx(results.ltrX0.l, 0) && approx(results.ltrX0.r, 1) && approx(results.ltrXMid.l, 1) && approx(results.ltrXMid.r, 1) && approx(results.ltrXMax.r, 0)], + ['RTL fade-start maps to physical right only', hasRight(results.rtlStart0) && noLeft(results.rtlStart0)], + ['RTL fade-start starts inactive then reveals', approx(results.rtlStart0.r, 0) && approx(results.rtlStartMid.r, 1)], + ['RTL fade-end maps to physical left only', hasLeft(results.rtlEnd0) && noRight(results.rtlEnd0)], + ['RTL fade-end starts present then retracts at end', approx(results.rtlEnd0.l, 1) && approx(results.rtlEndMax.l, 0)], + ['RTL fade-x uses both horizontal layers', hasLeft(results.rtlXMid) && hasRight(results.rtlXMid)], + ['RTL fade-x is active in middle and inactive at opposite ends', approx(results.rtlX0.r, 0) && approx(results.rtlX0.l, 1) && approx(results.rtlXMid.l, 1) && approx(results.rtlXMid.r, 1) && approx(results.rtlXMax.l, 0)], + ['nested dir override: LTR scroller inside RTL parent maps start to physical left', hasLeft(results.nestedLtrStart0) && noRight(results.nestedLtrStart0) && approx(results.nestedLtrStart0.l, 0) && approx(results.nestedLtrStartMid.l, 1)], + ['non-scrollable horizontal content leaves both horizontal amounts inactive', approx(results.short.l, 0) && approx(results.short.r, 0)], + ['horizontal timelines use scroll(self inline)', /scroll\(self inline\)/.test(results.ltrXMid.animationTimeline) && /scroll\(self inline\)/.test(results.rtlXMid.animationTimeline)], +] + +console.log(JSON.stringify(results, null, 2)) +console.log('\n=== HORIZONTAL RTL CHECKS ===') +let pass = 0 +for (const [label, ok] of checks) { + console.log(`${ok ? 'PASS' : 'FAIL'} ${label}`) + if (ok) pass++ +} +console.log(`\n${pass}/${checks.length} passed`) +process.exit(pass === checks.length ? 0 : 1) diff --git a/build/verify-page.mjs b/build/verify-page.mjs index afb1632..bcb0ef8 100644 --- a/build/verify-page.mjs +++ b/build/verify-page.mjs @@ -9,6 +9,9 @@ * - the advanced specimen can switch to one edge without dragging in the * other mask layers * - the horizontal rail starts on Recede and still moves selection on click + * - the advanced Direction control swaps fade-start/fade-end to the opposite + * physical edge under RTL, and the safeguard activates a horizontal edge + * when none is set * * Usage: node build/verify-page.mjs */ @@ -38,7 +41,7 @@ const structure = await page.evaluate(() => { } }) -const demoEdgeUtilities = ['fade-t', 'fade-b', 'fade-l', 'fade-r'] +const demoEdgeUtilities = ['fade-top', 'fade-bottom', 'fade-start', 'fade-end'] const missingDemoEdgeUtilities = demoEdgeUtilities.filter((className) => { return !new RegExp(`\\.${className}\\s*\\{`).test(demoCss) }) @@ -46,7 +49,7 @@ const singleEdgeSpecimen = await page.evaluate(async () => { const specimen = document.querySelector('[data-demo="type-specimen"]') if (!specimen) return null - specimen.className = 'fade-t fade-size-2xl fade-range-md thin-scroll type-scale-sample h-64 overflow-auto p-5 sm:h-72' + specimen.className = 'fade-top fade-size-2xl fade-travel-md thin-scroll type-scale-sample h-64 overflow-auto p-5 sm:h-72' specimen.scrollTop = 96 specimen.scrollLeft = 96 await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))) @@ -54,10 +57,10 @@ const singleEdgeSpecimen = await page.evaluate(async () => { const cs = getComputedStyle(specimen) return { maskComposite: cs.maskComposite || cs.webkitMaskComposite, - hasMaskT: cs.getPropertyValue('--tw-fade-mask-t').trim() !== '', - hasMaskB: cs.getPropertyValue('--tw-fade-mask-b').trim() !== '', - hasMaskL: cs.getPropertyValue('--tw-fade-mask-l').trim() !== '', - hasMaskR: cs.getPropertyValue('--tw-fade-mask-r').trim() !== '', + maskT: cs.getPropertyValue('--tw-fade-mask-t').trim(), + maskB: cs.getPropertyValue('--tw-fade-mask-b').trim(), + maskL: cs.getPropertyValue('--tw-fade-mask-l').trim(), + maskR: cs.getPropertyValue('--tw-fade-mask-r').trim(), } }) @@ -89,9 +92,69 @@ const railSelection = await page.evaluate(() => { } }) +// The advanced Direction control drives the real radios + edge toggles, not a +// synthetic panel (verify-horizontal-rtl.mjs covers the CSS routing in +// isolation). This asserts the demo wiring: flipping LTR -> RTL migrates the +// `end` fade to the opposite physical edge, and the safeguard activates a +// horizontal edge when none is set so the toggle never looks dead. +const dirControl = await page.evaluate(async () => { + const specimen = document.querySelector('[data-demo="type-specimen"]') + const ltrRadio = document.querySelector('[data-fade-dir="ltr"]') + const rtlRadio = document.querySelector('[data-fade-dir="rtl"]') + const endToggle = document.querySelector('[data-fade-edge-toggle="end"]') + const startToggle = document.querySelector('[data-fade-edge-toggle="start"]') + if (!specimen || !ltrRadio || !rtlRadio || !endToggle || !startToggle) return null + + const settle = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r))) + const horizontalPressed = () => + endToggle.getAttribute('aria-pressed') === 'true' || + startToggle.getAttribute('aria-pressed') === 'true' + const masks = () => { + const cs = getComputedStyle(specimen) + return { + maskL: cs.getPropertyValue('--tw-fade-mask-l').trim(), + maskR: cs.getPropertyValue('--tw-fade-mask-r').trim(), + } + } + const scrollMid = () => { + const max = specimen.scrollWidth - specimen.clientWidth + specimen.scrollLeft = getComputedStyle(specimen).direction === 'rtl' ? -(max / 2) : max / 2 + } + + // LTR baseline: enable the `end` edge so a horizontal mask layer exists. + ltrRadio.click() + if (endToggle.getAttribute('aria-pressed') !== 'true') endToggle.click() + scrollMid() + await settle() + const ltr = { dir: specimen.getAttribute('dir'), ...masks() } + + // Flip to RTL: the same `end` edge must migrate to the opposite physical side. + rtlRadio.click() + scrollMid() + await settle() + const rtl = { dir: specimen.getAttribute('dir'), ...masks() } + + // Safeguard: from a clean no-horizontal-edge state, RTL auto-enables one. + ltrRadio.click() + if (endToggle.getAttribute('aria-pressed') === 'true') endToggle.click() + if (startToggle.getAttribute('aria-pressed') === 'true') startToggle.click() + await settle() + const horizontalBefore = horizontalPressed() + rtlRadio.click() + scrollMid() + await settle() + const safeguard = { horizontalBefore, horizontalAfter: horizontalPressed(), ...masks() } + + return { ltr, rtl, safeguard } +}) + await browser.close() const transparent = (c) => c === 'rgba(0, 0, 0, 0)' || c === 'transparent' +// A horizontal mask layer is "active" when its gradient points inward: +// physical-left fades "to right", physical-right fades "to left". +const hasLeftMask = (m) => !!m && /to right/.test(m.maskL) +const hasRightMask = (m) => !!m && /to left/.test(m.maskR) const checks = [ ['demo build does not emit private .tw-fade-mask utility', !/\.tw-fade-mask\s*\{/.test(demoCss), '.tw-fade-mask absent'], @@ -103,16 +166,16 @@ const checks = [ ['body is the real scroll container', structure.bodyIsScroller, String(structure.bodyIsScroller)], ['surface is on (non-transparent)', !transparent(structure.htmlBg), structure.htmlBg], ['body is transparent (mask reveals the surface)', transparent(structure.bodyBg), structure.bodyBg], - ['body does not own a page-level fade utility', !/\bfade-[tblrxy]/.test(structure.bodyClass), structure.bodyClass], + ['body does not own a page-level fade utility', !/\bfade(?:\b|-)/.test(structure.bodyClass), structure.bodyClass], [ 'advanced demo supports a single top fade', - singleEdgeSpecimen?.hasMaskT === true && - singleEdgeSpecimen.hasMaskB === false && - singleEdgeSpecimen.hasMaskL === false && - singleEdgeSpecimen.hasMaskR === false && + /to bottom/.test(singleEdgeSpecimen?.maskT || '') && + !/to top/.test(singleEdgeSpecimen?.maskB || '') && + !/to right/.test(singleEdgeSpecimen?.maskL || '') && + !/to left/.test(singleEdgeSpecimen?.maskR || '') && /intersect/.test(singleEdgeSpecimen.maskComposite || ''), singleEdgeSpecimen - ? `t:${singleEdgeSpecimen.hasMaskT} b:${singleEdgeSpecimen.hasMaskB} l:${singleEdgeSpecimen.hasMaskL} r:${singleEdgeSpecimen.hasMaskR}` + ? `t:${/to bottom/.test(singleEdgeSpecimen.maskT)} b:${/to top/.test(singleEdgeSpecimen.maskB)} l:${/to right/.test(singleEdgeSpecimen.maskL)} r:${/to left/.test(singleEdgeSpecimen.maskR)}` : 'missing specimen', ], [ @@ -131,6 +194,29 @@ const checks = [ railSelection.after.filter(Boolean).length === 1, railSelection ? `${railSelection.count} cards, selected:${railSelection.selectedLabel}` : 'missing rail cards', ], + [ + 'advanced Direction control swaps the end fade to the opposite edge in RTL', + dirControl && + dirControl.ltr.dir === 'ltr' && + hasRightMask(dirControl.ltr) && + !hasLeftMask(dirControl.ltr) && + dirControl.rtl.dir === 'rtl' && + hasLeftMask(dirControl.rtl) && + !hasRightMask(dirControl.rtl), + dirControl + ? `ltr(R:${hasRightMask(dirControl.ltr)} L:${hasLeftMask(dirControl.ltr)}) -> rtl(L:${hasLeftMask(dirControl.rtl)} R:${hasRightMask(dirControl.rtl)})` + : 'missing direction control', + ], + [ + 'advanced Direction RTL safeguard activates a horizontal edge when none is set', + dirControl && + dirControl.safeguard.horizontalBefore === false && + dirControl.safeguard.horizontalAfter === true && + (hasLeftMask(dirControl.safeguard) || hasRightMask(dirControl.safeguard)), + dirControl + ? `before:${dirControl.safeguard.horizontalBefore} after:${dirControl.safeguard.horizontalAfter} mask(L:${hasLeftMask(dirControl.safeguard)} R:${hasRightMask(dirControl.safeguard)})` + : 'missing direction control', + ], ] console.log('Demo page: static file smoke') diff --git a/build/verify.mjs b/build/verify.mjs index 46e134f..62e79d7 100644 --- a/build/verify.mjs +++ b/build/verify.mjs @@ -18,7 +18,7 @@ async function probe(selector, { top, left, noSnap } = {}) { // Some probes measure the fade as a pure function of scroll offset. Snap // would pull a small programmatic scroll to the nearest row center, so the // exact offset (and thus the partial fade) depends on row height rather - // than the plugin. Disable snap for those to test the ramp itself. + // than the plugin. Disable snap for those to test the travel itself. const prevSnap = el.style.scrollSnapType if (disableSnap) el.style.scrollSnapType = 'none' if (scrollTop != null) el.scrollTop = scrollTop @@ -36,10 +36,10 @@ async function probe(selector, { top, left, noSnap } = {}) { b: num('--tw-fade-b'), l: num('--tw-fade-l'), r: num('--tw-fade-r'), - hasMaskT: cs.getPropertyValue('--tw-fade-mask-t').trim() !== '', - hasMaskB: cs.getPropertyValue('--tw-fade-mask-b').trim() !== '', - hasMaskL: cs.getPropertyValue('--tw-fade-mask-l').trim() !== '', - hasMaskR: cs.getPropertyValue('--tw-fade-mask-r').trim() !== '', + hasMaskT: /to bottom/.test(cs.getPropertyValue('--tw-fade-mask-t')), + hasMaskB: /to top/.test(cs.getPropertyValue('--tw-fade-mask-b')), + hasMaskL: /to right/.test(cs.getPropertyValue('--tw-fade-mask-l')), + hasMaskR: /to left/.test(cs.getPropertyValue('--tw-fade-mask-r')), maskImage: cs.maskImage, maskComposite: cs.maskComposite || cs.webkitMaskComposite, } @@ -58,7 +58,7 @@ const listMax = listTop.maxScrollTop results.list_top = listTop results.list_mid = await probe('[data-demo="list"]', { top: Math.round(listMax / 2) }) results.list_bottom = await probe('[data-demo="list"]', { top: listMax }) -// Measure the fade ramp at a small offset with snap off: this asserts the +// Measure the fade travel at a small offset with snap off: this asserts the // plugin reveals the top edge progressively, independent of where snap would // settle (which shifts with row height). results.list_quarter = await probe('[data-demo="list"]', { top: 25, noSnap: true }) diff --git a/demo/demo-animations.js b/demo/demo-animations.js index 8dc4fbe..55f353e 100644 --- a/demo/demo-animations.js +++ b/demo/demo-animations.js @@ -85,12 +85,12 @@ const spring = { restDelta: 0.001, }, swatch: { - // Background-picker thumbnails (ζ≈0.66): a single, lively overshoot on + // Background-picker thumbnails (ζ≈0.89): a restrained overshoot on // hover/press, and a pop on selection (driven by a transient over-target, - // not a velocity kick) that settles in one clean bounce. + // not a velocity kick) that settles without feeling rubbery. type: "spring", - stiffness: 560, - damping: 26, + stiffness: 520, + damping: 34, mass: 0.7, restDelta: 0.001, }, @@ -301,58 +301,68 @@ if (!prefersReducedMotion) { const round = (value) => Number(value.toFixed(3)); const lerp = (from, to, amount) => from + (to - from) * amount; const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value); + // Short sine-matched spans remove the center-crossing curvature jump while + // keeping the old half-wave Bezier's tuned 45px visual reach. + const sineSegmentsPerCycle = 8; + const tunedAmplitudeScale = 0.75; const currentScrollProgress = () => { const max = scroller.scrollHeight - scroller.clientHeight; return max > 0 ? Math.min(1, Math.max(0, scroller.scrollTop / max)) : 0; }; + const columnCubics = (centerX, sign0, amplitude, wavelength, cycleCount) => { + const visualAmplitude = amplitude * tunedAmplitudeScale; + const segmentCount = sineSegmentsPerCycle * cycleCount; + const segmentHeight = wavelength / sineSegmentsPerCycle; + const phaseStep = (Math.PI * 2) / sineSegmentsPerCycle; + const cubics = []; + + for (let segment = 0; segment < segmentCount; segment += 1) { + const phase0 = segment * phaseStep; + const phase1 = phase0 + phaseStep; + const y0 = segment * segmentHeight; + const y1 = (segment + 1) * segmentHeight; + const x0 = centerX + sign0 * visualAmplitude * Math.sin(phase0); + const x3 = centerX + sign0 * visualAmplitude * Math.sin(phase1); + const dx0 = sign0 * visualAmplitude * Math.cos(phase0) * phaseStep; + const dx1 = sign0 * visualAmplitude * Math.cos(phase1) * phaseStep; + + cubics.push({ + p0: { x: x0, y: y0 }, + p1: { x: x0 + dx0 / 3, y: y0 + segmentHeight / 3 }, + p2: { x: x3 - dx1 / 3, y: y1 - segmentHeight / 3 }, + p3: { x: x3, y: y1 }, + }); + } + + return cubics; + }; + + const cubicsPath = (cubics) => { + let d = `M${round(cubics[0].p0.x)} ${round(cubics[0].p0.y)}`; + for (const c of cubics) { + d += `C${round(c.p1.x)} ${round(c.p1.y)} ${round(c.p2.x)} ${round(c.p2.y)} ${round(c.p3.x)} ${round(c.p3.y)}`; + } + return d; + }; + const buildWave = (amplitude, wavelength) => { - const halfWave = wavelength / 2; - const segmentCount = 2 * Math.round(params.cycles); + const cycleCount = Math.round(params.cycles); const columnCount = Math.round(params.columns); const tileWidth = Math.round(params.spacing) * columnCount; - const tileHeight = Math.round(params.cycles) * wavelength; + const tileHeight = cycleCount * wavelength; let d = ""; for (let column = 0; column < columnCount; column += 1) { const centerX = params.spacing * column + params.spacing / 2; const initialSign = params.stagger && column % 2 === 1 ? -1 : 1; - d += `M${round(centerX)} 0`; - d += `C${round(centerX + initialSign * amplitude)} ${round(0.25 * halfWave)} ${round(centerX + initialSign * amplitude)} ${round(0.75 * halfWave)} ${round(centerX)} ${round(halfWave)}`; - - for (let segment = 1; segment < segmentCount; segment += 1) { - const sign = initialSign * (segment % 2 === 0 ? 1 : -1); - d += `S${round(centerX + sign * amplitude)} ${round(segment * halfWave + 0.75 * halfWave)} ${round(centerX)} ${round((segment + 1) * halfWave)}`; - } + d += cubicsPath( + columnCubics(centerX, initialSign, amplitude, wavelength, cycleCount), + ); } return { d, tileWidth, tileHeight }; }; - // Explicit cubic segments for one wave column (same shape as the stroke - // path), so the negative-space strips trace the exact wave edges. - const columnCubics = (centerX, sign0, amplitude, halfWave, segmentCount) => { - const cubics = [ - { - p0: { x: centerX, y: 0 }, - p1: { x: centerX + sign0 * amplitude, y: 0.25 * halfWave }, - p2: { x: centerX + sign0 * amplitude, y: 0.75 * halfWave }, - p3: { x: centerX, y: halfWave }, - }, - ]; - let prevP2 = cubics[0].p2; - let prevP3 = cubics[0].p3; - for (let segment = 1; segment < segmentCount; segment += 1) { - const sign = sign0 * (segment % 2 === 0 ? 1 : -1); - const cp1 = { x: 2 * prevP3.x - prevP2.x, y: 2 * prevP3.y - prevP2.y }; - const cp2 = { x: centerX + sign * amplitude, y: segment * halfWave + 0.75 * halfWave }; - const end = { x: centerX, y: (segment + 1) * halfWave }; - cubics.push({ p0: prevP3, p1: cp1, p2: cp2, p3: end }); - prevP2 = cp2; - prevP3 = end; - } - return cubics; - }; - const shiftCubics = (cubics, dx) => cubics.map((c) => ({ p0: { x: c.p0.x + dx, y: c.p0.y }, @@ -392,11 +402,10 @@ if (!prefersReducedMotion) { const buildStrips = (amplitude, wavelength, fieldWidth, fieldHeight) => { if (!fieldWidth || !fieldHeight) return ""; - const halfWave = wavelength / 2; const spacing = Math.round(params.spacing); - const segmentCount = 2 * (Math.ceil(fieldHeight / wavelength) + 1); - const totalHeight = segmentCount * halfWave; - const base = columnCubics(0, 1, amplitude, halfWave, segmentCount); + const cycleCount = Math.ceil(fieldHeight / wavelength) + 1; + const totalHeight = cycleCount * wavelength; + const base = columnCubics(0, 1, amplitude, wavelength, cycleCount); const columnMax = Math.ceil(fieldWidth / spacing) + 1; let markup = ""; for (let column = -1; column < columnMax; column += 1) { diff --git a/demo/demo-controls.js b/demo/demo-controls.js index 4d9c7ff..0bda881 100644 --- a/demo/demo-controls.js +++ b/demo/demo-controls.js @@ -22,10 +22,10 @@ const installCopyButton = document.querySelector("[data-copy-install]"); const installCopyIconStack = document.querySelector(".install-copy-icon-stack"); const installCopyStatus = document.querySelector("[data-copy-install-status]"); const fadeDepthSlider = document.querySelector("[data-fade-depth-slider]"); -const fadeRangeSlider = document.querySelector("[data-fade-range-slider]"); +const fadeTravelSlider = document.querySelector("[data-fade-travel-slider]"); const fadeOptionLabel = document.querySelector("[data-fade-option-label]"); const fadeDepthValue = document.querySelector("[data-fade-depth-value]"); -const fadeRangeValue = document.querySelector("[data-fade-range-value]"); +const fadeTravelValue = document.querySelector("[data-fade-travel-value]"); const fadeSpecimen = document.querySelector('[data-demo="type-specimen"]'); const fadeEdgeButtons = Array.from( document.querySelectorAll("[data-fade-edge-toggle]"), @@ -54,32 +54,32 @@ const fadeDepthSizes = [ aria: "quadruple extra large", }, ]; -const fadeRanges = [ - { className: "fade-range-sm", value: "sm", aria: "small" }, - { className: "fade-range-md", value: "md", aria: "medium" }, - { className: "fade-range-lg", value: "lg", aria: "large" }, +const fadeTravels = [ + { className: "fade-travel-sm", value: "sm", aria: "small" }, + { className: "fade-travel-md", value: "md", aria: "medium" }, + { className: "fade-travel-lg", value: "lg", aria: "large" }, { - className: "fade-range-xl", + className: "fade-travel-xl", value: "xl", aria: "extra large", }, { - className: "fade-range-2xl", + className: "fade-travel-2xl", value: "2xl", aria: "double extra large", }, { - className: "fade-range-3xl", + className: "fade-travel-3xl", value: "3xl", aria: "triple extra large", }, { - className: "fade-range-4xl", + className: "fade-travel-4xl", value: "4xl", aria: "quadruple extra large", }, ]; -const fadeEdgeOrder = ["t", "r", "b", "l"]; +const fadeEdgeOrder = ["top", "end", "bottom", "start"]; const fadeEdgeClassNames = fadeEdgeOrder.map((edge) => `fade-${edge}`); const faviconSize = 256; const faviconCells = 32; @@ -380,8 +380,8 @@ function setSwatchScale(swatch, scale, animated = true, transition) { transition ?? animation.spring?.swatch ?? { type: "spring", - stiffness: 560, - damping: 26, + stiffness: 520, + damping: 34, mass: 0.7, restDelta: 0.001, }, @@ -393,7 +393,7 @@ function syncSwatchSpring(swatch, animated = true) { const state = swatchStateFor(swatch); const checked = swatch.getAttribute("aria-checked") === "true"; const scale = state.pressed - ? 0.9 + ? 0.95 : checked ? 1.08 : state.hovered @@ -404,8 +404,9 @@ function syncSwatchSpring(swatch, animated = true) { function springSwatchSelection(swatch) { // Pop: spring past the checked rest scale, then settle back to it. The - // transient over-target (1.18 → 1.08) reads as a deliberate selection bounce. - setSwatchScale(swatch, 1.18, true); + // transient over-target (1.13 → 1.08) reads as selection without a rubbery + // rebound. + setSwatchScale(swatch, 1.13, true); window.setTimeout(() => { syncSwatchSpring(swatch, true); }, 130); @@ -857,9 +858,12 @@ function selectLexiconRow(nextRow, { animated = true, scroll = true } = {}) { function edgeUtilities(activeEdges) { const has = (edge) => activeEdges.includes(edge); - if (has("t") && has("r") && has("b") && has("l")) return ["fade-xy"]; - if (has("t") && has("b") && activeEdges.length === 2) return ["fade-y"]; - if (has("l") && has("r") && activeEdges.length === 2) return ["fade-x"]; + if (has("top") && has("end") && has("bottom") && has("start")) + return ["fade"]; + if (has("top") && has("bottom") && activeEdges.length === 2) + return ["fade-y"]; + if (has("start") && has("end") && activeEdges.length === 2) + return ["fade-x"]; return activeEdges.map((edge) => `fade-${edge}`); } @@ -886,7 +890,7 @@ function syncEdgeGradientDepth(depthIndex) { function fadeUtilityParts(className) { const prefix = - ["fade-size-", "fade-range-", "fade-"].find((candidate) => + ["fade-size-", "fade-travel-", "fade-"].find((candidate) => className.startsWith(candidate), ) ?? ""; return { @@ -944,16 +948,16 @@ function flashFadeOptionParts(parts) { } } -function setFadeOptionLabel(edgeClasses, sizeClass, rangeClass) { +function setFadeOptionLabel(edgeClasses, sizeClass, travelClass) { const nextState = { edgeClasses: [...edgeClasses], sizeClass, - rangeClass, + travelClass, }; const previous = fadeOptionReadoutState; const flashTargets = []; const fragment = document.createDocumentFragment(); - const fullClassName = [...edgeClasses, sizeClass, rangeClass].join(" "); + const fullClassName = [...edgeClasses, sizeClass, travelClass].join(" "); const previousEdgeClasses = previous?.edgeClasses ?? []; const edgeStemChanged = previous && @@ -985,8 +989,8 @@ function setFadeOptionLabel(edgeClasses, sizeClass, rangeClass) { appendToken(sizeClass, previous?.sizeClass === sizeClass ? "none" : "stem"); appendToken( - rangeClass, - previous?.rangeClass === rangeClass ? "none" : "stem", + travelClass, + previous?.travelClass === travelClass ? "none" : "stem", ); fadeOptionLabel.replaceChildren(fragment); @@ -998,10 +1002,10 @@ function setFadeOptionLabel(edgeClasses, sizeClass, rangeClass) { function setFadeControls() { if ( !fadeDepthSlider || - !fadeRangeSlider || + !fadeTravelSlider || !fadeOptionLabel || !fadeDepthValue || - !fadeRangeValue || + !fadeTravelValue || !fadeSpecimen ) return; @@ -1010,12 +1014,12 @@ function setFadeControls() { fadeDepthSizes.length - 1, Math.max(0, Number(fadeDepthSlider.value)), ); - const rangeIndex = Math.min( - fadeRanges.length - 1, - Math.max(0, Number(fadeRangeSlider.value)), + const travelIndex = Math.min( + fadeTravels.length - 1, + Math.max(0, Number(fadeTravelSlider.value)), ); const nextSize = fadeDepthSizes[depthIndex]; - const nextRange = fadeRanges[rangeIndex]; + const nextTravel = fadeTravels[travelIndex]; const activeEdges = fadeEdgeOrder.filter((edge) => { const button = fadeEdgeButtons.find( (candidate) => candidate.dataset.fadeEdgeToggle === edge, @@ -1026,25 +1030,25 @@ function setFadeControls() { const utilityClasses = [ ...edgeClasses, nextSize.className, - nextRange.className, + nextTravel.className, ]; fadeSpecimen.classList.remove( ...fadeEdgeClassNames, + "fade", "fade-y", "fade-x", - "fade-xy", ...fadeDepthSizes.map((size) => size.className), - ...fadeRanges.map((range) => range.className), + ...fadeTravels.map((travel) => travel.className), ); fadeSpecimen.classList.add(...utilityClasses); - setFadeOptionLabel(edgeClasses, nextSize.className, nextRange.className); + setFadeOptionLabel(edgeClasses, nextSize.className, nextTravel.className); fadeDepthValue.textContent = nextSize.value; - fadeRangeValue.textContent = nextRange.value; + fadeTravelValue.textContent = nextTravel.value; fadeDepthSlider.setAttribute("aria-valuetext", nextSize.aria); - fadeRangeSlider.setAttribute("aria-valuetext", nextRange.aria); + fadeTravelSlider.setAttribute("aria-valuetext", nextTravel.aria); syncSliderKnob(fadeDepthSlider); - syncSliderKnob(fadeRangeSlider); + syncSliderKnob(fadeTravelSlider); syncEdgeGradientDepth(depthIndex); } @@ -1353,7 +1357,7 @@ fadeDepthSlider?.addEventListener("input", (event) => { setFadeControls(); }); -fadeRangeSlider?.addEventListener("input", () => { +fadeTravelSlider?.addEventListener("input", () => { setFadeControls(); }); @@ -1371,6 +1375,79 @@ for (const button of fadeEdgeButtons) { setFadeControls(); +// --- Direction (LTR / RTL) ------------------------------------------------- +// The plugin routes RTL entirely through the :dir(rtl) selector, so flipping +// the scroll container's `dir` attribute is all it takes to make fade-start / +// fade-end swap physical edges. This control sets nothing but `dir` and lets +// the CSS do the rest. +const dirGroup = document.querySelector("[data-fade-dir-group]"); +if (dirGroup && fadeSpecimen) { + const dirOptions = Array.from(dirGroup.querySelectorAll("[data-fade-dir]")); + + const hasHorizontalEdge = () => + fadeEdgeButtons.some( + (button) => + (button.dataset.fadeEdgeToggle === "start" || + button.dataset.fadeEdgeToggle === "end") && + button.getAttribute("aria-pressed") === "true", + ); + + // Block-axis edges (top / bottom) never flip with direction, so switching to + // RTL with no horizontal edge active would look like nothing happened. Enable + // `end` — it's the edge visible at the specimen's rest scroll position, so the + // left/right swap reads at once. The toggle springs on, so the user sees the + // cause rather than a hidden side effect. + const ensureHorizontalEdgeVisible = () => { + if (hasHorizontalEdge()) return; + const endButton = fadeEdgeButtons.find( + (button) => button.dataset.fadeEdgeToggle === "end", + ); + if (!endButton) return; + endButton.setAttribute("aria-pressed", "true"); + springEdgeToggleSelection(endButton); + setFadeControls(); + }; + + const setDir = (dir, focus = false) => { + fadeSpecimen.setAttribute("dir", dir); + for (const option of dirOptions) { + const active = option.dataset.fadeDir === dir; + option.setAttribute("aria-checked", active ? "true" : "false"); + // Roving tabindex: only the selected radio stays in the tab order. + option.tabIndex = active ? 0 : -1; + if (active && focus) option.focus(); + } + if (dir === "rtl") ensureHorizontalEdgeVisible(); + }; + + for (const option of dirOptions) { + option.tabIndex = option.getAttribute("aria-checked") === "true" ? 0 : -1; + option.addEventListener("click", () => setDir(option.dataset.fadeDir)); + } + + // Arrow keys move and select within the group, matching native radio semantics. + dirGroup.addEventListener("keydown", (event) => { + const current = dirOptions.indexOf(document.activeElement); + if (current === -1) return; + let next = null; + if (event.key === "ArrowRight" || event.key === "ArrowDown") { + next = (current + 1) % dirOptions.length; + } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") { + next = (current - 1 + dirOptions.length) % dirOptions.length; + } + if (next === null) return; + event.preventDefault(); + setDir(dirOptions[next].dataset.fadeDir, true); + }); + + // Establish the initial direction on the specimen (LTR by default) without + // tripping the RTL safeguard. + const initialDir = + dirOptions.find((option) => option.getAttribute("aria-checked") === "true") + ?.dataset.fadeDir ?? "ltr"; + fadeSpecimen.setAttribute("dir", initialDir); +} + function syncFloatingPalette() { setFloatingPaletteVisible(document.body.scrollTop > 24); } diff --git a/demo/demo.css b/demo/demo.css index d1a1a07..0330af7 100644 --- a/demo/demo.css +++ b/demo/demo.css @@ -190,7 +190,20 @@ modern wide-gamut OKLCH/P3 color syntax. */ } html[data-surface] { - --demo-wave-fallback: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'128'%20height%3D'1629'%20viewBox%3D'0%200%20128%201629'%3E%3Cpath%20d%3D'M64%200C124%2067.875%20124%20203.625%2064%20271.5S4%20475.125%2064%20543S124%20746.625%2064%20814.5S4%201018.125%2064%201086S124%201289.625%2064%201357.5S4%201561.125%2064%201629'%20fill%3D'none'%20stroke%3D'%23ffffff'%20stroke-opacity%3D'0.075'%20stroke-width%3D'1'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%20shape-rendering%3D'geometricPrecision'%2F%3E%3C%2Fsvg%3E"); + --demo-wave-fallback: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22128%22%20height%3D%221629%22%20viewBox%3D%220%200%20128%201629%22%3E%3Cpath%20d%3D%22M64%200C75.781%2022.625%2087.489%2045.25%2095.82%2067.875C104.15%2090.5%20109%20113.125%20109%20135.75C109%20158.375%20104.15%20181%2095.82%20203.625C87.489%20226.25%2075.781%20248.875%2064%20271.5C52.219%20294.125%2040.511%20316.75%2032.18%20339.375C23.85%20362%2019%20384.625%2019%20407.25C19%20429.875%2023.85%20452.5%2032.18%20475.125C40.511%20497.75%2052.219%20520.375%2064%20543C75.781%20565.625%2087.489%20588.25%2095.82%20610.875C104.15%20633.5%20109%20656.125%20109%20678.75C109%20701.375%20104.15%20724%2095.82%20746.625C87.489%20769.25%2075.781%20791.875%2064%20814.5C52.219%20837.125%2040.511%20859.75%2032.18%20882.375C23.85%20905%2019%20927.625%2019%20950.25C19%20972.875%2023.85%20995.5%2032.18%201018.125C40.511%201040.75%2052.219%201063.375%2064%201086C75.781%201108.625%2087.489%201131.25%2095.82%201153.875C104.15%201176.5%20109%201199.125%20109%201221.75C109%201244.375%20104.15%201267%2095.82%201289.625C87.489%201312.25%2075.781%201334.875%2064%201357.5C52.219%201380.125%2040.511%201402.75%2032.18%201425.375C23.85%201448%2019%201470.625%2019%201493.25C19%201515.875%2023.85%201538.5%2032.18%201561.125C40.511%201583.75%2052.219%201606.375%2064%201629%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-opacity%3D%220.075%22%20stroke-width%3D%221%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20shape-rendering%3D%22geometricPrecision%22%2F%3E%3C%2Fsvg%3E"); + --demo-container-shadow: + 0 1px 1px rgb(0 0 0 / 0.161), + 0 5px 10px -9px rgb(0 0 0 / 0.23); + --demo-inner-border-size: 1px; + --demo-inner-border-light-alpha: 0.18; + --demo-inner-border-mid-alpha: 0.045; + --demo-inner-border-dark-alpha: 0.22; + --demo-inner-border-opacity: 0.9; + --demo-inner-border-blend-mode: soft-light; + /* Distance over which the top light / bottom shadow fade out as they wrap the + corners. ~2x the corner radius, so the highlight eases off just past the curve + instead of hard-stopping at the corner miter. */ + --demo-inner-border-taper: 1.75rem; /* Color for the stepped wave-strip fills. Matches the white wave strokes on every (dark) surface; overridable per theme for a tinted gradation. */ @@ -529,6 +542,83 @@ h3 { font-variant-numeric: tabular-nums slashed-zero; } +.creator-note-link { + --shimmer-color: var(--demo-text-strong); + --shimmer-speed: 40; + --shimmer-repeat-delay: 1400; + --shimmer-spread: 96px; + --shimmer-track-width: 220px; + position: relative; + display: inline-block; + color: color-mix(in oklab, var(--demo-text-primary) 68%, transparent); + text-decoration: none; + transition: + color 160ms ease; +} + +.creator-note-link:hover { + color: color-mix(in oklab, var(--demo-text-primary) 82%, transparent); +} + +.creator-note-tooltip-root { + vertical-align: baseline; +} + +.creator-note-tooltip { + left: 50%; + width: min(16rem, calc(100vw - 2rem)); + transform: translateX(-50%) translateY(0.25rem); +} + +.creator-note-tooltip-root:hover .creator-note-tooltip, +.creator-note-tooltip-root:focus-within .creator-note-tooltip { + transform: translateX(-50%) translateY(0); +} + +.creator-note-underline { + --shimmer-color: color-mix( + in oklab, + var(--demo-text-strong) 48%, + transparent + ); + --shimmer-speed: 40; + --shimmer-repeat-delay: 1400; + --shimmer-spread: 96px; + --shimmer-track-width: 220px; + position: absolute; + right: 0; + bottom: 0.08em; + left: 0; + height: 1px; + border-radius: 999px; + background-color: color-mix( + in oklab, + var(--demo-text-primary) 18%, + transparent + ); + color: var(--demo-text-primary); + pointer-events: none; +} + +.creator-note-link:focus-visible { + outline: 2px solid color-mix(in oklab, var(--demo-focus) 78%, transparent); + outline-offset: 3px; + border-radius: 0.25rem; +} + +@media (prefers-reduced-motion: reduce) { + .creator-note-link.shimmer { + animation: none; + background: none; + -webkit-text-fill-color: currentColor; + } + + .creator-note-underline.shimmer-bg { + background-image: none; + animation: none; + } +} + .fade-option-label { color: var(--demo-text-subtle); } @@ -560,6 +650,8 @@ h3 { } .install-snippet { + --demo-inner-border-light-alpha: 0.2; + --demo-inner-border-dark-alpha: 0.2; position: relative; display: inline-flex; align-self: flex-start; @@ -582,9 +674,11 @@ h3 { 95%, transparent ); - box-shadow: none; + box-shadow: var(--demo-container-shadow); isolation: isolate; - transition: background-color 180ms ease; + transition: + background-color 180ms ease, + box-shadow 180ms ease; } .install-snippet-sweep { @@ -820,19 +914,38 @@ h3 { } .social-icon-link { + --demo-inner-border-light-alpha: 0.2; + --demo-inner-border-dark-alpha: 0.2; + --social-icon-bg: color-mix( + in oklab, + color-mix( + in oklab, + var(--demo-page-bg) 92%, + var(--demo-text-primary) 8% + ) + 96%, + transparent + ); + --social-icon-bg-hover: color-mix( + in oklab, + color-mix( + in oklab, + var(--demo-page-bg) 84%, + var(--demo-text-primary) 16% + ) + 96%, + transparent + ); position: relative; display: grid; width: 2.25rem; height: 2.25rem; flex: 0 0 2.25rem; place-items: center; - border: 1px solid var(--demo-border); + border: 0; border-radius: 9999px; - background-color: color-mix( - in oklab, - var(--demo-panel-bg) 54%, - transparent - ); + background-color: var(--social-icon-bg); + box-shadow: var(--demo-container-shadow); color: color-mix(in oklab, var(--demo-text-strong) 68%, transparent); backdrop-filter: blur(12px); cursor: pointer; @@ -841,16 +954,17 @@ h3 { transition: color 180ms ease, background-color 180ms ease, - border-color 180ms ease; + box-shadow 180ms ease; } .social-icon-link:hover { - border-color: var(--demo-border-strong); - background-color: var(--demo-panel-strong); + background-color: var(--social-icon-bg-hover); color: var(--demo-text-strong); } .social-icon-link svg { + position: relative; + z-index: 2; width: 1rem; height: 1rem; fill: currentColor; @@ -881,7 +995,93 @@ h3 { border-color 180ms ease; } +:is( + .install-snippet, + .social-icon-link, + .tuning-card, + .feature-column, + .support-tooltip, + .floating-surface-palette + ) { + position: relative; + isolation: isolate; +} + +/* Inner bevel highlight, drawn as two uniform-color borders rather than one per-side + border. Why a border at all: a border is the only ring primitive that follows the box's + corner-shape — round OR superellipse(1.35) — so it tracks the squircle exactly (a mask / + box-shadow ring always corners as a circle and mismatches the host). corner-shape is NOT + inherited and border-radius: inherit copies only the radius, so corner-shape: inherit is + required here; unsupported browsers drop it and both host + highlight stay round. + Why two layers + masks: a per-side border meets its top/side/bottom colors at a hard + miter seam at each corner. Instead, ::before paints a uniform light border and ::after a + uniform dark one, each faded with a linear-gradient mask so the top catch and bottom + shadow taper off smoothly as they wrap the corner. */ +:is( + .install-snippet, + .social-icon-link, + .tuning-card, + .feature-column, + .support-tooltip, + .floating-surface-palette + )::before, +:is( + .install-snippet, + .social-icon-link, + .tuning-card, + .feature-column, + .support-tooltip, + .floating-surface-palette + )::after { + content: ""; + position: absolute; + inset: 0; + z-index: 1; + border-radius: inherit; + corner-shape: inherit; + pointer-events: none; + border: var(--demo-inner-border-size) solid transparent; + mix-blend-mode: var(--demo-inner-border-blend-mode); + opacity: var(--demo-inner-border-opacity); +} + +/* Light catch on the top edge, easing out as it rounds the top corners. */ +:is( + .install-snippet, + .social-icon-link, + .tuning-card, + .feature-column, + .support-tooltip, + .floating-surface-palette + )::before { + border-color: rgb(255 255 255 / var(--demo-inner-border-light-alpha)); + mask-image: linear-gradient( + to bottom, + #000, + transparent var(--demo-inner-border-taper) + ); +} + +/* Shadow on the bottom edge, easing out as it rounds the bottom corners. */ +:is( + .install-snippet, + .social-icon-link, + .tuning-card, + .feature-column, + .support-tooltip, + .floating-surface-palette + )::after { + border-color: rgb(0 0 0 / var(--demo-inner-border-dark-alpha)); + mask-image: linear-gradient( + to top, + #000, + transparent var(--demo-inner-border-taper) + ); +} + .tuning-card { + --demo-inner-border-light-alpha: 0.2; + --demo-inner-border-dark-alpha: 0.2; border: 0; background-color: color-mix( in oklab, @@ -893,7 +1093,7 @@ h3 { 95%, transparent ); - box-shadow: 0 18px 48px rgb(0 0 0 / 0.22); + box-shadow: var(--demo-container-shadow); transition: background-color 180ms ease, box-shadow 180ms ease; @@ -1034,6 +1234,7 @@ h3 { align-self: center; min-width: 0; color: var(--demo-text-soft); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.875rem; line-height: 1.4; opacity: 0.34; @@ -1084,7 +1285,7 @@ h3 { } } -/* Messages thread — the fade-t companion to the lexicon. It starts pinned to +/* Messages thread — the fade-y companion to the lexicon. It starts pinned to the bottom (newest reply crisp) so scrolling up dissolves older messages into the top edge only. Height matches the lexicon so the two cards balance. */ .chat-thread { @@ -1139,6 +1340,8 @@ h3 { words dissolve and the pattern shows straight through their edges. A static gradient overlay can only paint one flat color and would leave a band here. */ .anybg-stage { + --anybg-stage-height: 26rem; + position: relative; /* Transparent base so the page background (and its wave) shows through every gap in the lattice above. */ @@ -1154,12 +1357,12 @@ h3 { A plane-filling lattice can never present only whole motifs behind a hard rectangular edge — any straight container edge cuts through them at almost every size. So rather than hard-clip, we DISSOLVE the pattern's own four edges - using the plugin itself: the SVG carries `fade-xy fade-static`, which pins a + using the plugin itself: the SVG carries `fade fade-always`, which pins a permanent all-edge fade (no scroll needed). A motif the box slices is already transparent at the cut, so it feathers out instead of being chopped — at any width/height and through live resize, zero JS, and it dogfoods tw-fade. The - default md fade size (3rem) matches the rail's fade-x, so the pattern and the - words dissolve along one shared edge. */ + shared 2xl fade size matches the rail's fade-x, so the pattern and the words + dissolve along one shared edge. */ .anybg-pattern { position: absolute; inset: 0; @@ -1175,9 +1378,9 @@ h3 { z-index: 1; display: flex; align-items: center; - gap: 2.75rem; - height: 13rem; - padding: 0 2.25rem; + gap: clamp(4rem, 7vw, 6.5rem); + height: var(--anybg-stage-height); + padding: 0 clamp(3rem, 6vw, 5rem); background: transparent; } @@ -1186,9 +1389,9 @@ h3 { color: #fafafa; font-family: "Zalando Sans SemiExpanded", ui-sans-serif, system-ui, sans-serif; - font-size: clamp(2.5rem, 1.5rem + 3.2vw, 4rem); + font-size: clamp(4.75rem, 2.75rem + 6.5vw, 8rem); font-weight: 700; - letter-spacing: -0.02em; + letter-spacing: 0; line-height: 1; white-space: nowrap; user-select: none; @@ -1268,6 +1471,10 @@ h3 { } .floating-surface-palette { + --demo-inner-border-light-alpha: 0.2; + --demo-inner-border-mid-alpha: 0.04; + --demo-inner-border-dark-alpha: 0.24; + --demo-inner-border-opacity: 0.736; --palette-glass-filter: url(#palette-glass-filter); --palette-glass-blur: 3px; --palette-glass-brightness: 1.12; @@ -1285,10 +1492,8 @@ h3 { isolation: isolate; background: rgb(255 255 255 / 0.05); box-shadow: - inset 0 0 0 1px rgb(255 255 255 / 0.1), - inset 0 1px 0 rgb(255 255 255 / 0.15), - inset 0 -18px 28px rgb(255 255 255 / 0.02), - 0 12px 36px rgb(0 0 0 / 0.16); + 0 1px 1px rgb(0 0 0 / 0.14), + 0 5px 10px -9px rgb(0 0 0 / 0.18); -webkit-backdrop-filter: var(--palette-glass-filter) blur(var(--palette-glass-blur)) brightness(var(--palette-glass-brightness)) @@ -1449,6 +1654,10 @@ h3 { } .support-tooltip { + --demo-inner-border-light-alpha: 0.22; + --demo-inner-border-mid-alpha: 0.04; + --demo-inner-border-dark-alpha: 0.22; + --demo-inner-border-opacity: 0.828; position: absolute; bottom: calc(100% + 0.5rem); left: 0; @@ -1456,8 +1665,11 @@ h3 { width: min(13rem, calc(100vw - 2rem)); pointer-events: none; border-radius: var(--demo-radius-sm); - border: 1px solid var(--demo-border); + border: 0; background: var(--demo-panel-strong); + box-shadow: + 0 1px 1px rgb(0 0 0 / 0.14), + 0 5px 10px -9px rgb(0 0 0 / 0.2); padding: 0.625rem 0.75rem; color: var(--demo-text-primary); font-size: 0.75rem; @@ -1891,31 +2103,31 @@ label.option-control { inset 0 0 0 4px color-mix(in oklab, var(--demo-focus) 62%, transparent); } -.edge-toggle-button[data-fade-edge-toggle="t"] { +.edge-toggle-button[data-fade-edge-toggle="top"] { clip-path: polygon(0 0, 100% 0, 50% 50%); } -.edge-toggle-button[data-fade-edge-toggle="r"] { +.edge-toggle-button[data-fade-edge-toggle="end"] { clip-path: polygon(100% 0, 100% 100%, 50% 50%); } -.edge-toggle-button[data-fade-edge-toggle="b"] { +.edge-toggle-button[data-fade-edge-toggle="bottom"] { clip-path: polygon(100% 100%, 0 100%, 50% 50%); } -.edge-toggle-button[data-fade-edge-toggle="l"] { +.edge-toggle-button[data-fade-edge-toggle="start"] { clip-path: polygon(0 100%, 0 0, 50% 50%); } -.edge-toggle-button[data-fade-edge-toggle="r"] .edge-toggle-svg { +.edge-toggle-button[data-fade-edge-toggle="end"] .edge-toggle-svg { --edge-rotation: 90deg; } -.edge-toggle-button[data-fade-edge-toggle="b"] .edge-toggle-svg { +.edge-toggle-button[data-fade-edge-toggle="bottom"] .edge-toggle-svg { --edge-rotation: 180deg; } -.edge-toggle-button[data-fade-edge-toggle="l"] .edge-toggle-svg { +.edge-toggle-button[data-fade-edge-toggle="start"] .edge-toggle-svg { --edge-rotation: -90deg; } @@ -1972,6 +2184,56 @@ label.option-control { ); } +.dir-toggle-group { + display: inline-grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + width: min(100%, 10rem); + gap: 0.1875rem; + padding: 0.1875rem; + border: 1px solid + color-mix(in oklab, var(--demo-border-strong) 34%, transparent); + border-radius: 0.5rem; + background: color-mix(in oklab, var(--demo-page-bg) 35%, transparent); +} + +.dir-toggle-option { + border: 0; + border-radius: 0.375rem; + padding: 0.375rem 0; + background: transparent; + color: var(--demo-text-muted); + font: inherit; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.06em; + line-height: 1; + text-align: center; + cursor: pointer; + transition: + color 160ms ease, + background-color 160ms ease; + -webkit-tap-highlight-color: transparent; +} + +.dir-toggle-option[aria-checked="true"] { + color: var(--demo-accent-text); + background: color-mix(in oklab, var(--demo-accent-text) 16%, transparent); +} + +.dir-toggle-option:focus-visible { + outline: none; + box-shadow: + 0 0 0 2px var(--demo-page-bg), + 0 0 0 4px color-mix(in oklab, var(--demo-focus) 62%, transparent); +} + +@media (prefers-reduced-motion: reduce) { + .dir-toggle-option { + transition: none; + } +} + .type-scale-sample { container-type: inline-size; padding: 2.5rem; @@ -1980,7 +2242,9 @@ label.option-control { .type-scale-content { width: max-content; min-width: 100%; - padding-right: 5rem; + /* Trailing breathing room follows the inline direction so the fade + dissolves over space, not glyphs, in both LTR and RTL. */ + padding-inline-end: 5rem; } .type-scale-row { @@ -1999,6 +2263,10 @@ label.option-control { } .feature-column { + --demo-inner-border-light-alpha: 0.16; + --demo-inner-border-mid-alpha: 0.035; + --demo-inner-border-dark-alpha: 0.18; + --demo-inner-border-opacity: 0.828; --feature-card-bg: color-mix( in oklab, color-mix( @@ -2059,7 +2327,7 @@ label.option-control { padding: 1.5rem; border-radius: var(--demo-radius-lg); background-color: var(--feature-card-bg); - box-shadow: 0 18px 48px rgb(0 0 0 / 0.22); + box-shadow: var(--demo-container-shadow); transition: background-color 180ms ease, box-shadow 180ms ease; @@ -2282,7 +2550,7 @@ class drawers up from below when composed / drops away when removed. */ } .type-scale-content { - padding-right: 3rem; + padding-inline-end: 3rem; } } diff --git a/demo/index.html b/demo/index.html index 9dcf6fe..e4db2ec 100644 --- a/demo/index.html +++ b/demo/index.html @@ -461,6 +461,27 @@ > Elegant, scroll-driven edge masking. One class. Zero JavaScript. +
+ By one of the creators of + + tw-shimmer + CSS-only shimmer effects, with 500k+ weekly npm + downloads. + .

- hey! finally trying tw-fade 👋 + this feed has a hard edge

- oh nice — welcome + add fade-y to the scroll area

- these scroll lists are slick + do i need another wrapper?

- thanks, it’s all CSS + no. use the element that scrolls

- this thread fades at the edges - while i scroll 👀 + at the top, does the fade stay?

- that’s tw-fade ✨ + no, the top fade clears

- some scroll library? + and at the end?

- nope — a pure CSS mask + the bottom fade clears

- zero JavaScript + the newest message stays clear

- so how does it know i’m - scrolling? + horizontal tabs too?

- scroll-driven CSS — the fade is - scroll-gated + use fade-x on the rail

- does it soften both edges? + can i make the fade wider?

- fade-y — one class on the - container + set a larger size

- but the newest message stays - sharp 🔥 + top edge by itself?

- pinned to the bottom, yep — scroll - up and both edges dissolve ↕︎ + use fade-top

@@ -1199,15 +1214,15 @@ fade-xyy fade-range-fade-travel-md2xl @@ -1239,7 +1254,7 @@
- -
+
+ Direction +
+ + +
+
+ +
@@ -1994,14 +2043,14 @@ x="91" y="84" > - xy + top - t + end - r + start - range-2xl + travel-2xl

- Axis, size, range, and clear-zone utilities compose + Axis, size, travel, and clear-zone utilities compose directly on the scroll container.

@@ -2149,9 +2198,9 @@ -
+
Partial support in stable Firefox: the mask - renders, but the reveal falls back to a static + renders, but the travel falls back to a static always-on fade. diff --git a/demo/input.css b/demo/input.css index f9f0be2..020d01f 100644 --- a/demo/input.css +++ b/demo/input.css @@ -1,8 +1,9 @@ @import "tailwindcss" source(none); @import "../src/tw-fade.css"; +@import "tw-shimmer"; @import "./vendor/tw-glass.css"; /* Scan the demo markup for utility classes. */ @source "./index.html"; /* The Advanced sides control composes these classes at runtime. */ -@source inline("fade-{t,b,l,r}"); +@source inline("fade-{top,bottom,start,end}"); diff --git a/demo/styles.css b/demo/styles.css index bd205e9..a8c3beb 100644 --- a/demo/styles.css +++ b/demo/styles.css @@ -35,13 +35,13 @@ --fade-size-2xl: calc(var(--spacing, 0.25rem) * 24); --fade-size-3xl: calc(var(--spacing, 0.25rem) * 32); --fade-size-4xl: calc(var(--spacing, 0.25rem) * 40); - --fade-range-sm: calc(var(--spacing, 0.25rem) * 8); - --fade-range-md: calc(var(--spacing, 0.25rem) * 12); - --fade-range-lg: calc(var(--spacing, 0.25rem) * 16); - --fade-range-xl: calc(var(--spacing, 0.25rem) * 20); - --fade-range-2xl: calc(var(--spacing, 0.25rem) * 24); - --fade-range-3xl: calc(var(--spacing, 0.25rem) * 32); - --fade-range-4xl: calc(var(--spacing, 0.25rem) * 40); + --fade-travel-sm: calc(var(--spacing, 0.25rem) * 8); + --fade-travel-md: calc(var(--spacing, 0.25rem) * 12); + --fade-travel-lg: calc(var(--spacing, 0.25rem) * 16); + --fade-travel-xl: calc(var(--spacing, 0.25rem) * 20); + --fade-travel-2xl: calc(var(--spacing, 0.25rem) * 24); + --fade-travel-3xl: calc(var(--spacing, 0.25rem) * 32); + --fade-travel-4xl: calc(var(--spacing, 0.25rem) * 40); } } @layer base { @@ -199,24 +199,6 @@ .static { position: static; } - .container { - width: 100%; - @media (width >= 40rem) { - max-width: 40rem; - } - @media (width >= 48rem) { - max-width: 48rem; - } - @media (width >= 64rem) { - max-width: 64rem; - } - @media (width >= 80rem) { - max-width: 80rem; - } - @media (width >= 96rem) { - max-width: 96rem; - } - } .mx-auto { margin-inline: auto; } @@ -274,496 +256,72 @@ .shrink-0 { flex-shrink: 0; } - .fade-xy { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); - } - .fade-x { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + .shimmer { + --_gradient-width: calc(var(--_spread) + var(--shimmer-track-height) * tan(var(--shimmer-angle))); + --_active-distance: calc(var(--shimmer-track-width, 200px) + var(--_gradient-width)); + --_duration: var(--shimmer-duration, calc(var(--_active-distance) / var(--_speed) / 1px * 1000)); + --_repeat-delay: var(--shimmer-repeat-delay, calc(20000 / var(--_speed))); + --_repeat-delay-px: calc(var(--_repeat-delay) * var(--_active-distance) / var(--_duration)); + --_xy-offset-px: calc((var(--shimmer-x, 0) + var(--shimmer-y, 0) * tan(var(--shimmer-angle))) * 1px); + --_bg-width: calc( + 100% + + var(--shimmer-track-width, 100%) + + var(--_gradient-width) + + var(--_repeat-delay-px) + ); + --_position: calc( + var(--shimmer-track-width, (100% - var(--_gradient-width) - var(--_repeat-delay-px)) / 2) + + var(--_gradient-width) / 2 + + var(--_repeat-delay-px) + - var(--_xy-offset-px) + ); + &:not(.shimmer-bg) { + --_speed: var(--shimmer-speed, 200); + --_spread: var(--shimmer-spread, calc(4ch + 80px)); + --_bg: currentColor; + --_fg: var(--shimmer-color, oklch(from currentColor l c h / calc(alpha * 0.2))); + background-clip: text; + -webkit-text-fill-color: transparent; } - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); - } - .fade-y { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + @media (prefers-color-scheme: dark) { + &:not(.shimmer-bg) { + --_fg: var(--shimmer-color, oklch(from currentColor max(0.8, calc(l + 0.4)) c h / calc(alpha + 0.4))); + } } - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); - } - .fade-b { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; + @-moz-document url-prefix() { + --_duration: var(--shimmer-duration, calc(375000 / var(--_speed))); } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + --_mix-96: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-96: color-mix(in oklch, var(--_fg), var(--_bg) 96%); } - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); - } - .fade-l { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; + --_mix-83: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-83: color-mix(in oklch, var(--_fg), var(--_bg) 83%); } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + --_mix-67: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-67: color-mix(in oklch, var(--_fg), var(--_bg) 67%); } - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); - } - .fade-r { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; + --_mix-50: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-50: color-mix(in oklch, var(--_fg), var(--_bg) 50%); } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + --_mix-33: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-33: color-mix(in oklch, var(--_fg), var(--_bg) 33%); } - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); - } - .fade-t { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; + --_mix-17: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-17: color-mix(in oklch, var(--_fg), var(--_bg) 17%); } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + --_mix-4: var(--_fg); + @supports (color: color-mix(in lab, red, red)) { + --_mix-4: color-mix(in oklch, var(--_fg), var(--_bg) 4%); } - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); + background: linear-gradient( calc(90deg + var(--shimmer-angle)), var(--_bg) calc(var(--_position) - var(--_spread) * 0.5), var(--_mix-96) calc(var(--_position) - var(--_spread) * 0.44), var(--_mix-83) calc(var(--_position) - var(--_spread) * 0.37), var(--_mix-67) calc(var(--_position) - var(--_spread) * 0.31), var(--_mix-50) calc(var(--_position) - var(--_spread) * 0.25), var(--_mix-33) calc(var(--_position) - var(--_spread) * 0.19), var(--_mix-17) calc(var(--_position) - var(--_spread) * 0.12), var(--_mix-4) calc(var(--_position) - var(--_spread) * 0.06), var(--_fg) var(--_position), var(--_mix-4) calc(var(--_position) + var(--_spread) * 0.06), var(--_mix-17) calc(var(--_position) + var(--_spread) * 0.12), var(--_mix-33) calc(var(--_position) + var(--_spread) * 0.19), var(--_mix-50) calc(var(--_position) + var(--_spread) * 0.25), var(--_mix-67) calc(var(--_position) + var(--_spread) * 0.31), var(--_mix-83) calc(var(--_position) + var(--_spread) * 0.37), var(--_mix-96) calc(var(--_position) + var(--_spread) * 0.44), var(--_bg) calc(var(--_position) + var(--_spread) * 0.5) ) 0 0 / var(--_bg-width) 100% no-repeat; + animation: tw-shimmer 1s linear 0s infinite backwards; + animation-duration: calc((var(--_duration) + var(--_repeat-delay)) * 1ms); } .flex-col { flex-direction: column; @@ -961,38 +519,19 @@ .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } - .fade-static { - --tw-fade-t: 1 !important; - --tw-fade-b: 1 !important; - --tw-fade-l: 1 !important; - --tw-fade-r: 1 !important; - animation-name: none; + .shimmer-bg { + --_speed: var(--shimmer-speed, 1000); + --_spread: var(--shimmer-spread, 480px); + --_bg: transparent; + --_fg: var(--shimmer-color, oklch(from currentColor 0 c h / 0.06)); + @media (prefers-color-scheme: dark) { + --_fg: var(--shimmer-color, oklch(from currentColor 0 c h / 0.30)); + } } .select-none { -webkit-user-select: none; user-select: none; } - .fade-range-2xl { - --tw-fade-range: var(--fade-range-2xl); - } - .fade-range-3xl { - --tw-fade-range: var(--fade-range-3xl); - } - .fade-range-4xl { - --tw-fade-range: var(--fade-range-4xl); - } - .fade-range-lg { - --tw-fade-range: var(--fade-range-lg); - } - .fade-range-md { - --tw-fade-range: var(--fade-range-md); - } - .fade-range-sm { - --tw-fade-range: var(--fade-range-sm); - } - .fade-range-xl { - --tw-fade-range: var(--fade-range-xl); - } .fade-size-2xl { --tw-fade-size: var(--fade-size-2xl); } @@ -1014,6 +553,27 @@ .fade-size-xl { --tw-fade-size: var(--fade-size-xl); } + .fade-travel-2xl { + --tw-fade-travel: var(--fade-travel-2xl); + } + .fade-travel-3xl { + --tw-fade-travel: var(--fade-travel-3xl); + } + .fade-travel-4xl { + --tw-fade-travel: var(--fade-travel-4xl); + } + .fade-travel-lg { + --tw-fade-travel: var(--fade-travel-lg); + } + .fade-travel-md { + --tw-fade-travel: var(--fade-travel-md); + } + .fade-travel-sm { + --tw-fade-travel: var(--fade-travel-sm); + } + .fade-travel-xl { + --tw-fade-travel: var(--fade-travel-xl); + } .sm\:mr-4 { @media (width >= 40rem) { margin-right: calc(var(--spacing) * 4); @@ -1108,39 +668,51 @@ syntax: "*"; inherits: false; } -@property --tw-fade-size-t { +@property --tw-fade-size-top { + syntax: "*"; + inherits: false; +} +@property --tw-fade-size-bottom { syntax: "*"; inherits: false; } -@property --tw-fade-size-b { +@property --tw-fade-size-start { syntax: "*"; inherits: false; } -@property --tw-fade-size-l { +@property --tw-fade-size-end { syntax: "*"; inherits: false; } -@property --tw-fade-size-r { +@property --tw-fade-travel { syntax: "*"; inherits: false; } -@property --tw-fade-range { +@property --tw-fade-clear { syntax: "*"; inherits: false; } -@property --tw-fade-clear-t { +@property --tw-fade-clear-y { syntax: "*"; inherits: false; } -@property --tw-fade-clear-b { +@property --tw-fade-clear-x { syntax: "*"; inherits: false; } -@property --tw-fade-clear-l { +@property --tw-fade-clear-top { syntax: "*"; inherits: false; } -@property --tw-fade-clear-r { +@property --tw-fade-clear-bottom { + syntax: "*"; + inherits: false; +} +@property --tw-fade-clear-start { + syntax: "*"; + inherits: false; +} +@property --tw-fade-clear-end { syntax: "*"; inherits: false; } @@ -1160,7 +732,71 @@ syntax: "*"; inherits: false; } -@keyframes tw-fade-reveal-t { +@property --tw-fade-ltr-start-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-animation-range { + syntax: "*"; + inherits: false; +} +@keyframes tw-fade-travel-t { from { --tw-fade-t: 0; } @@ -1168,7 +804,7 @@ --tw-fade-t: 1; } } -@keyframes tw-fade-reveal-b { +@keyframes tw-fade-travel-b { from { --tw-fade-b: 1; } @@ -1176,7 +812,7 @@ --tw-fade-b: 0; } } -@keyframes tw-fade-reveal-l { +@keyframes tw-fade-travel-l { from { --tw-fade-l: 0; } @@ -1184,7 +820,7 @@ --tw-fade-l: 1; } } -@keyframes tw-fade-reveal-r { +@keyframes tw-fade-travel-r { from { --tw-fade-r: 1; } @@ -1192,11 +828,274 @@ --tw-fade-r: 0; } } +@keyframes tw-fade-travel-r-leading { + from { + --tw-fade-r: 0; + } + to { + --tw-fade-r: 1; + } +} +@keyframes tw-fade-travel-l-trailing { + from { + --tw-fade-l: 1; + } + to { + --tw-fade-l: 0; + } +} @layer utilities { - .fade-t, .fade-b, .fade-l, .fade-r, .fade-y, .fade-x, .fade-xy { - --tw-fade-size-default: var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)); - --tw-fade-range-active: var(--tw-fade-range, var(--fade-range-md, calc(var(--spacing, 0.25rem) * 12))); + .fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end { + --tw-fade-size-default: min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12))); + --tw-fade-travel-active: var(--tw-fade-travel, var(--fade-travel-sm, calc(var(--spacing, 0.25rem) * 8))); + --tw-fade-identity-mask: linear-gradient(#000, #000); + --tw-fade-edge-size-t: var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-b: var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-l: var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-r: var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-clear-t: var(--tw-fade-clear-top, var(--tw-fade-clear-y, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-b: var(--tw-fade-clear-bottom, var(--tw-fade-clear-y, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-l: var(--tw-fade-clear-start, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-r: var(--tw-fade-clear-end, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); + --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); + --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); + --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); + --tw-fade-onset: 8; + --tw-fade-alpha-t: min(1, calc(var(--tw-fade-t) * var(--tw-fade-onset))); + --tw-fade-alpha-b: min(1, calc(var(--tw-fade-b) * var(--tw-fade-onset))); + --tw-fade-alpha-l: min(1, calc(var(--tw-fade-l) * var(--tw-fade-onset))); + --tw-fade-alpha-r: min(1, calc(var(--tw-fade-r) * var(--tw-fade-onset))); + --tw-fade-gradient-t: linear-gradient( + to bottom, + #000 0 var(--tw-fade-edge-clear-t), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t))) var(--tw-fade-edge-clear-t), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.983)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.933)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.854)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.75)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.629)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.5)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.371)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.25)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.146)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.067)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.017)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) + ); + --tw-fade-gradient-b: linear-gradient( + to top, + #000 0 var(--tw-fade-edge-clear-b), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b))) var(--tw-fade-edge-clear-b), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.983)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.933)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.854)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.75)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.629)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.5)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.371)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.25)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.146)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.067)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.017)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) + ); + --tw-fade-gradient-l: linear-gradient( + to right, + #000 0 var(--tw-fade-edge-clear-l), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l))) var(--tw-fade-edge-clear-l), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.983)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.933)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.854)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.75)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.629)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.5)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.371)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.25)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.146)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.067)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.017)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) + ); + --tw-fade-gradient-r: linear-gradient( + to left, + #000 0 var(--tw-fade-edge-clear-r), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r))) var(--tw-fade-edge-clear-r), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.983)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.933)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.854)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.75)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.629)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.5)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.371)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.25)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.146)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.067)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.017)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) + ); + --tw-fade-mask-l: var(--tw-fade-ltr-start-layer, var(--tw-fade-identity-mask)); + --tw-fade-mask-r: var(--tw-fade-ltr-end-layer, var(--tw-fade-identity-mask)); + --tw-fade-animation-l: var(--tw-fade-ltr-start-animation, none); + --tw-fade-animation-r: var(--tw-fade-ltr-end-animation, none); + --tw-fade-timeline-l: var(--tw-fade-ltr-start-timeline, none); + --tw-fade-timeline-r: var(--tw-fade-ltr-end-timeline, none); + --tw-fade-animation-range-l: var(--tw-fade-ltr-start-animation-range, normal); + --tw-fade-animation-range-r: var(--tw-fade-ltr-end-animation-range, normal); + --tw-fade-animation-t: none; + --tw-fade-animation-b: none; + --tw-fade-timeline-t: none; + --tw-fade-timeline-b: none; + --tw-fade-animation-range-t: normal; + --tw-fade-animation-range-b: normal; + mask-image: var(--tw-fade-mask-t, var(--tw-fade-identity-mask)), var(--tw-fade-mask-b, var(--tw-fade-identity-mask)), var(--tw-fade-mask-l, var(--tw-fade-identity-mask)), var(--tw-fade-mask-r, var(--tw-fade-identity-mask)); + mask-composite: intersect; + mask-repeat: no-repeat; + mask-size: 100% 100%; + } + .fade:where(:dir(rtl)), .fade-y:where(:dir(rtl)), .fade-top:where(:dir(rtl)), .fade-bottom:where(:dir(rtl)), .fade-x:where(:dir(rtl)), .fade-start:where(:dir(rtl)), .fade-end:where(:dir(rtl)) { + --tw-fade-edge-size-l: var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-r: var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-clear-l: var(--tw-fade-clear-end, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-r: var(--tw-fade-clear-start, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-mask-l: var(--tw-fade-rtl-end-layer, var(--tw-fade-identity-mask)); + --tw-fade-mask-r: var(--tw-fade-rtl-start-layer, var(--tw-fade-identity-mask)); + --tw-fade-animation-l: var(--tw-fade-rtl-end-animation, none); + --tw-fade-animation-r: var(--tw-fade-rtl-start-animation, none); + --tw-fade-timeline-l: var(--tw-fade-rtl-end-timeline, none); + --tw-fade-timeline-r: var(--tw-fade-rtl-start-timeline, none); + --tw-fade-animation-range-l: var(--tw-fade-rtl-end-animation-range, normal); + --tw-fade-animation-range-r: var(--tw-fade-rtl-start-animation-range, normal); + } + .fade, .fade-y, .fade-top { + --tw-fade-mask-t: var(--tw-fade-gradient-t); + --tw-fade-animation-t: tw-fade-travel-t; + --tw-fade-timeline-t: scroll(self y); + --tw-fade-animation-range-t: 0 var(--tw-fade-travel-active); + } + .fade, .fade-y, .fade-bottom { + --tw-fade-mask-b: var(--tw-fade-gradient-b); + --tw-fade-animation-b: tw-fade-travel-b; + --tw-fade-timeline-b: scroll(self y); + --tw-fade-animation-range-b: calc(100% - var(--tw-fade-travel-active)) 100%; + } + .fade, .fade-x, .fade-start { + --tw-fade-ltr-start-layer: var(--tw-fade-gradient-l); + --tw-fade-rtl-start-layer: var(--tw-fade-gradient-r); + --tw-fade-ltr-start-animation: tw-fade-travel-l; + --tw-fade-rtl-start-animation: tw-fade-travel-r-leading; + --tw-fade-ltr-start-timeline: scroll(self inline); + --tw-fade-rtl-start-timeline: scroll(self inline); + --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-travel-active); + --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-travel-active); + } + .fade, .fade-x, .fade-end { + --tw-fade-ltr-end-layer: var(--tw-fade-gradient-r); + --tw-fade-rtl-end-layer: var(--tw-fade-gradient-l); + --tw-fade-ltr-end-animation: tw-fade-travel-r; + --tw-fade-rtl-end-animation: tw-fade-travel-l-trailing; + --tw-fade-ltr-end-timeline: scroll(self inline); + --tw-fade-rtl-end-timeline: scroll(self inline); + --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%; + --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%; + } + @supports (animation-timeline: scroll()) { + .fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end { + animation: var(--tw-fade-animation-t, none) auto linear both, var(--tw-fade-animation-b, none) auto linear both, var(--tw-fade-animation-l, none) auto linear both, var(--tw-fade-animation-r, none) auto linear both; + animation-timeline: var(--tw-fade-timeline-t, none), var(--tw-fade-timeline-b, none), var(--tw-fade-timeline-l, none), var(--tw-fade-timeline-r, none); + animation-range: var(--tw-fade-animation-range-t, normal), var(--tw-fade-animation-range-b, normal), var(--tw-fade-animation-range-l, normal), var(--tw-fade-animation-range-r, normal); + } } + @supports not (animation-timeline: scroll()) { + .fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end { + --tw-fade-t: 1; + --tw-fade-b: 1; + --tw-fade-l: 1; + --tw-fade-r: 1; + } + } + .fade-none { + --tw-fade-t: 0 !important; + --tw-fade-b: 0 !important; + --tw-fade-l: 0 !important; + --tw-fade-r: 0 !important; + } + .fade-none-y { + --tw-fade-t: 0 !important; + --tw-fade-b: 0 !important; + } + .fade-none-x { + --tw-fade-l: 0 !important; + --tw-fade-r: 0 !important; + } + .fade-always { + --tw-fade-t: 1 !important; + --tw-fade-b: 1 !important; + --tw-fade-l: 1 !important; + --tw-fade-r: 1 !important; + } + .fade-always-y { + --tw-fade-t: 1 !important; + --tw-fade-b: 1 !important; + } + .fade-always-x { + --tw-fade-l: 1 !important; + --tw-fade-r: 1 !important; + } +} +@property --shimmer-track-height { + syntax: ''; + inherits: true; + initial-value: 200px; +} +@property --shimmer-angle { + syntax: ''; + inherits: true; + initial-value: 15deg; } @property --tw-glass-blur { syntax: ""; @@ -1360,9 +1259,16 @@ syntax: "*"; inherits: false; } +@keyframes tw-shimmer { + from { + background-position: 100% 0; + } +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { :root, :host { + --shimmer-track-height: 200px; + --shimmer-angle: 15deg; --glass-bg-opacity: 0.08; } *, ::before, ::after, ::backdrop { @@ -1404,19 +1310,38 @@ --tw-fade-size: initial; --tw-fade-size-y: initial; --tw-fade-size-x: initial; - --tw-fade-size-t: initial; - --tw-fade-size-b: initial; - --tw-fade-size-l: initial; - --tw-fade-size-r: initial; - --tw-fade-range: initial; - --tw-fade-clear-t: initial; - --tw-fade-clear-b: initial; - --tw-fade-clear-l: initial; - --tw-fade-clear-r: initial; + --tw-fade-size-top: initial; + --tw-fade-size-bottom: initial; + --tw-fade-size-start: initial; + --tw-fade-size-end: initial; + --tw-fade-travel: initial; + --tw-fade-clear: initial; + --tw-fade-clear-y: initial; + --tw-fade-clear-x: initial; + --tw-fade-clear-top: initial; + --tw-fade-clear-bottom: initial; + --tw-fade-clear-start: initial; + --tw-fade-clear-end: initial; --tw-fade-mask-t: initial; --tw-fade-mask-b: initial; --tw-fade-mask-l: initial; --tw-fade-mask-r: initial; + --tw-fade-ltr-start-layer: initial; + --tw-fade-ltr-end-layer: initial; + --tw-fade-rtl-start-layer: initial; + --tw-fade-rtl-end-layer: initial; + --tw-fade-ltr-start-animation: initial; + --tw-fade-ltr-end-animation: initial; + --tw-fade-rtl-start-animation: initial; + --tw-fade-rtl-end-animation: initial; + --tw-fade-ltr-start-timeline: initial; + --tw-fade-ltr-end-timeline: initial; + --tw-fade-rtl-start-timeline: initial; + --tw-fade-rtl-end-timeline: initial; + --tw-fade-ltr-start-animation-range: initial; + --tw-fade-ltr-end-animation-range: initial; + --tw-fade-rtl-start-animation-range: initial; + --tw-fade-rtl-end-animation-range: initial; --tw-glass-blur: 2px; --tw-glass-brightness: 1.05; --tw-glass-saturation: 1.2; diff --git a/demo/wave-lab.html b/demo/wave-lab.html index 761303f..4f2e245 100644 --- a/demo/wave-lab.html +++ b/demo/wave-lab.html @@ -436,27 +436,36 @@

wave background lab

// explicit cubic segments (p0..p3) that compose it. The cubics let us // build closed fill shapes — either lobes (curve→tile-edge) or strips // (the negative space *between* two adjacent wave lines). - function columnPath(cx, s0, a, h, segs) { - let stroke = `M${r(cx)} 0`; + // Short sine-matched spans remove the center-crossing curvature jump while + // keeping the old half-wave Bezier's tuned 45px visual reach. + const SINE_SEGMENTS_PER_CYCLE = 8; + const TUNED_AMPLITUDE_SCALE = 0.75; + + function columnPath(cx, s0, amp, wl, cycles) { + const a = amp * TUNED_AMPLITUDE_SCALE; + const segs = SINE_SEGMENTS_PER_CYCLE * cycles; + const segmentH = wl / SINE_SEGMENTS_PER_CYCLE; + const phaseStep = (Math.PI * 2) / SINE_SEGMENTS_PER_CYCLE; + let stroke = ""; const cubics = []; - const p0 = { x: cx, y: 0 }; - const p1 = { x: cx + s0 * a, y: 0.25 * h }; - const p2 = { x: cx + s0 * a, y: 0.75 * h }; - const p3 = { x: cx, y: h }; - stroke += `C${r(p1.x)} ${r(p1.y)} ${r(p2.x)} ${r(p2.y)} ${r(p3.x)} ${r(p3.y)}`; - cubics.push({ p0, p1, p2, p3 }); - - let prevP2 = p2; - let prevP3 = p3; - for (let i = 1; i < segs; i += 1) { - const sign = s0 * (i % 2 === 0 ? 1 : -1); - const cp1 = { x: 2 * prevP3.x - prevP2.x, y: 2 * prevP3.y - prevP2.y }; - const cp2 = { x: cx + sign * a, y: i * h + 0.75 * h }; - const end = { x: cx, y: (i + 1) * h }; - stroke += `S${r(cp2.x)} ${r(cp2.y)} ${r(end.x)} ${r(end.y)}`; - cubics.push({ p0: prevP3, p1: cp1, p2: cp2, p3: end }); - prevP2 = cp2; - prevP3 = end; + + for (let i = 0; i < segs; i += 1) { + const phase0 = i * phaseStep; + const phase1 = phase0 + phaseStep; + const y0 = i * segmentH; + const y1 = (i + 1) * segmentH; + const x0 = cx + s0 * a * Math.sin(phase0); + const x3 = cx + s0 * a * Math.sin(phase1); + const dx0 = s0 * a * Math.cos(phase0) * phaseStep; + const dx1 = s0 * a * Math.cos(phase1) * phaseStep; + const p0 = { x: x0, y: y0 }; + const p1 = { x: x0 + dx0 / 3, y: y0 + segmentH / 3 }; + const p2 = { x: x3 - dx1 / 3, y: y1 - segmentH / 3 }; + const p3 = { x: x3, y: y1 }; + + if (i === 0) stroke = `M${r(p0.x)} ${r(p0.y)}`; + stroke += `C${r(p1.x)} ${r(p1.y)} ${r(p2.x)} ${r(p2.y)} ${r(p3.x)} ${r(p3.y)}`; + cubics.push({ p0, p1, p2, p3 }); } return { stroke, cubics }; @@ -502,11 +511,9 @@

wave background lab

// they stay in a cheap repeating . Per-strip stepped shading can't // tile, so it's built separately as explicit geometry (buildSteppedStrips). function buildWave(amp, wl) { - const a = amp; - const h = wl / 2; - const segs = 2 * Math.round(params.cycles); + const cycles = Math.round(params.cycles); const S = Math.round(params.spacing); - const H = Math.round(params.cycles) * wl; + const H = cycles * wl; const cols = Math.round(params.columns); const tileW = S * cols; let strokeD = ""; @@ -514,7 +521,7 @@

wave background lab

for (let c = 0; c < cols; c += 1) { const cx = S * c + S / 2; const s0 = params.stagger && c % 2 === 1 ? -1 : 1; - const col = columnPath(cx, s0, a, h, segs); + const col = columnPath(cx, s0, amp, wl, cycles); strokeD += col.stroke; fillD += lobeFill(col, cx, S * c, S * (c + 1), H); } @@ -550,12 +557,10 @@

wave background lab

// darker. Built as explicit paths because per-strip shades can't tile. function buildSteppedStrips(amp, wl, Wf, Hf) { if (!Wf || !Hf) return ""; - const a = amp; - const h = wl / 2; const S = Math.round(params.spacing); - const segs = 2 * (Math.ceil(Hf / wl) + 1); // cover full field height + overscan - const totalH = segs * h; - const base = columnPath(0, 1, a, h, segs).cubics; // one wave centered at x=0 + const cycles = Math.ceil(Hf / wl) + 1; // cover full field height + overscan + const totalH = cycles * wl; + const base = columnPath(0, 1, amp, wl, cycles).cubics; // one wave centered at x=0 const kMax = Math.ceil(Wf / S) + 1; let out = ""; for (let k = -1; k < kMax; k += 1) { @@ -875,28 +880,31 @@

wave background lab

` var r = function (v) { return Number(Number(v).toFixed(3)); };\n` + ` var lerp = function (a, b, t) { return a + (b - a) * t; };\n` + ` var clamp01 = function (v) { return v < 0 ? 0 : v > 1 ? 1 : v; };\n` + + ` var SP = 8, AS = 0.75;\n` + + ` function columnCubics(cx, s0, amp, wl, cycles) {\n` + + ` var a = amp * AS, segs = SP * cycles, sh = wl / SP, step = Math.PI * 2 / SP, cs = [];\n` + + ` for (var i = 0; i < segs; i++) {\n` + + ` var p0a = i * step, p1a = p0a + step, y0 = i * sh, y1 = (i + 1) * sh;\n` + + ` var x0 = cx + s0 * a * Math.sin(p0a), x3 = cx + s0 * a * Math.sin(p1a);\n` + + ` var dx0 = s0 * a * Math.cos(p0a) * step, dx1 = s0 * a * Math.cos(p1a) * step;\n` + + ` cs.push({ p0: { x: x0, y: y0 }, p1: { x: x0 + dx0 / 3, y: y0 + sh / 3 }, p2: { x: x3 - dx1 / 3, y: y1 - sh / 3 }, p3: { x: x3, y: y1 } });\n` + + ` }\n` + + ` return cs;\n` + + ` }\n` + + ` function cubicsPath(cs) {\n` + + ` var d = "M" + r(cs[0].p0.x) + " " + r(cs[0].p0.y);\n` + + ` for (var i = 0; i < cs.length; i++) { var c = cs[i]; d += "C" + r(c.p1.x) + " " + r(c.p1.y) + " " + r(c.p2.x) + " " + r(c.p2.y) + " " + r(c.p3.x) + " " + r(c.p3.y); }\n` + + ` return d;\n` + + ` }\n` + ` function buildWave(amp, wl) {\n` + - ` var h = wl / 2, segs = 2 * Math.round(P.cycles), cols = Math.round(P.columns);\n` + - ` var tileW = Math.round(P.spacing) * cols, H = Math.round(P.cycles) * wl, d = "";\n` + + ` var cycles = Math.round(P.cycles), cols = Math.round(P.columns);\n` + + ` var tileW = Math.round(P.spacing) * cols, H = cycles * wl, d = "";\n` + ` for (var c = 0; c < cols; c++) {\n` + ` var cx = P.spacing * c + P.spacing / 2, s0 = P.stagger && c % 2 === 1 ? -1 : 1;\n` + - ` d += "M" + r(cx) + " 0C" + r(cx + s0 * amp) + " " + r(0.25 * h) + " " + r(cx + s0 * amp) + " " + r(0.75 * h) + " " + r(cx) + " " + r(h);\n` + - ` for (var i = 1; i < segs; i++) { var sn = s0 * (i % 2 === 0 ? 1 : -1); d += "S" + r(cx + sn * amp) + " " + r(i * h + 0.75 * h) + " " + r(cx) + " " + r((i + 1) * h); }\n` + + ` d += cubicsPath(columnCubics(cx, s0, amp, wl, cycles));\n` + ` }\n` + ` return { d: d, tileW: tileW, H: H };\n` + ` }\n` + - ` function columnCubics(cx, s0, amp, h, segs) {\n` + - ` var cs = [{ p0: { x: cx, y: 0 }, p1: { x: cx + s0 * amp, y: 0.25 * h }, p2: { x: cx + s0 * amp, y: 0.75 * h }, p3: { x: cx, y: h } }];\n` + - ` var pP2 = cs[0].p2, pP3 = cs[0].p3;\n` + - ` for (var i = 1; i < segs; i++) {\n` + - ` var sn = s0 * (i % 2 === 0 ? 1 : -1);\n` + - ` var cp1 = { x: 2 * pP3.x - pP2.x, y: 2 * pP3.y - pP2.y };\n` + - ` var cp2 = { x: cx + sn * amp, y: i * h + 0.75 * h };\n` + - ` var end = { x: cx, y: (i + 1) * h };\n` + - ` cs.push({ p0: pP3, p1: cp1, p2: cp2, p3: end }); pP2 = cp2; pP3 = end;\n` + - ` }\n` + - ` return cs;\n` + - ` }\n` + ` function shift(cs, dx) { return cs.map(function (c) { return { p0: { x: c.p0.x + dx, y: c.p0.y }, p1: { x: c.p1.x + dx, y: c.p1.y }, p2: { x: c.p2.x + dx, y: c.p2.y }, p3: { x: c.p3.x + dx, y: c.p3.y } }; }); }\n` + ` function stripPath(L, R, cxL, cxR, H) {\n` + ` var d = "M" + r(cxL) + " 0", i, c;\n` + @@ -914,9 +922,8 @@

wave background lab

` }\n` + ` function buildStrips(amp, wl, W, Hh) {\n` + ` if (!W || !Hh) return "";\n` + - ` var h = wl / 2, S = Math.round(P.spacing);\n` + - ` var segs = 2 * (Math.ceil(Hh / wl) + 1), totalH = segs * h;\n` + - ` var base = columnCubics(0, 1, amp, h, segs), out = "", kMax = Math.ceil(W / S) + 1;\n` + + ` var S = Math.round(P.spacing), cycles = Math.ceil(Hh / wl) + 1, totalH = cycles * wl;\n` + + ` var base = columnCubics(0, 1, amp, wl, cycles), out = "", kMax = Math.ceil(W / S) + 1;\n` + ` for (var k = -1; k < kMax; k++) {\n` + ` var cxL = k * S + S / 2, cxR = cxL + S;\n` + ` var d = stripPath(shift(base, cxL), shift(base, cxR), cxL, cxR, totalH);\n` + diff --git a/dist/tw-fade.css b/dist/tw-fade.css index a8573de..4bf11f5 100644 --- a/dist/tw-fade.css +++ b/dist/tw-fade.css @@ -1,761 +1,194 @@ /*! tw-fade — framework-free build for plain HTML / CDN. * Generated from src/tw-fade.css; do not edit by hand. */ @layer properties; -.fade-xy { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); -} -.fade-x { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); -} -.fade-y { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); -} -.fade-b { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); -} -.fade-l { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); -} -.fade-r { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); -} -.fade-t { - mask-image: var(--tw-fade-mask-t, linear-gradient(#000, #000)), var(--tw-fade-mask-b, linear-gradient(#000, #000)), var(--tw-fade-mask-l, linear-gradient(#000, #000)), var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; - @supports (animation-timeline: scroll()) { - animation: tw-fade-reveal-t auto linear both, tw-fade-reveal-b auto linear both, tw-fade-reveal-l auto linear both, tw-fade-reveal-r auto linear both; - animation-timeline: scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%, 0 var(--tw-fade-range-active), calc(100% - var(--tw-fade-range-active)) 100%; - } - @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; - } - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); -} -.fade-static { - --tw-fade-t: 1 !important; - --tw-fade-b: 1 !important; - --tw-fade-l: 1 !important; - --tw-fade-r: 1 !important; - animation-name: none; -} -.fade-clear-xy-2xl { - --tw-fade-clear-t: var(--fade-clear-2xl); - --tw-fade-clear-b: var(--fade-clear-2xl); - --tw-fade-clear-l: var(--fade-clear-2xl); - --tw-fade-clear-r: var(--fade-clear-2xl); -} -.fade-clear-xy-3xl { - --tw-fade-clear-t: var(--fade-clear-3xl); - --tw-fade-clear-b: var(--fade-clear-3xl); - --tw-fade-clear-l: var(--fade-clear-3xl); - --tw-fade-clear-r: var(--fade-clear-3xl); -} -.fade-clear-xy-4xl { - --tw-fade-clear-t: var(--fade-clear-4xl); - --tw-fade-clear-b: var(--fade-clear-4xl); - --tw-fade-clear-l: var(--fade-clear-4xl); - --tw-fade-clear-r: var(--fade-clear-4xl); -} -.fade-clear-xy-lg { - --tw-fade-clear-t: var(--fade-clear-lg); - --tw-fade-clear-b: var(--fade-clear-lg); - --tw-fade-clear-l: var(--fade-clear-lg); - --tw-fade-clear-r: var(--fade-clear-lg); -} -.fade-clear-xy-md { - --tw-fade-clear-t: var(--fade-clear-md); - --tw-fade-clear-b: var(--fade-clear-md); - --tw-fade-clear-l: var(--fade-clear-md); - --tw-fade-clear-r: var(--fade-clear-md); -} -.fade-clear-xy-sm { - --tw-fade-clear-t: var(--fade-clear-sm); - --tw-fade-clear-b: var(--fade-clear-sm); - --tw-fade-clear-l: var(--fade-clear-sm); - --tw-fade-clear-r: var(--fade-clear-sm); -} -.fade-clear-xy-var { - --tw-fade-clear-t: var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px))); - --tw-fade-clear-b: var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px))); - --tw-fade-clear-l: var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px))); - --tw-fade-clear-r: var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px))); -} -.fade-clear-xy-xl { - --tw-fade-clear-t: var(--fade-clear-xl); - --tw-fade-clear-b: var(--fade-clear-xl); - --tw-fade-clear-l: var(--fade-clear-xl); - --tw-fade-clear-r: var(--fade-clear-xl); -} -.fade-clear-xy-xs { - --tw-fade-clear-t: var(--fade-clear-xs); - --tw-fade-clear-b: var(--fade-clear-xs); - --tw-fade-clear-l: var(--fade-clear-xs); - --tw-fade-clear-r: var(--fade-clear-xs); +.fade-clear-2xl { + --tw-fade-clear: var(--fade-clear-2xl); } -.fade-clear-x-2xl { - --tw-fade-clear-l: var(--fade-clear-2xl); - --tw-fade-clear-r: var(--fade-clear-2xl); +.fade-clear-3xl { + --tw-fade-clear: var(--fade-clear-3xl); } -.fade-clear-x-3xl { - --tw-fade-clear-l: var(--fade-clear-3xl); - --tw-fade-clear-r: var(--fade-clear-3xl); +.fade-clear-4xl { + --tw-fade-clear: var(--fade-clear-4xl); } -.fade-clear-x-4xl { - --tw-fade-clear-l: var(--fade-clear-4xl); - --tw-fade-clear-r: var(--fade-clear-4xl); +.fade-clear-bottom-2xl { + --tw-fade-clear-bottom: var(--fade-clear-2xl); } -.fade-clear-x-lg { - --tw-fade-clear-l: var(--fade-clear-lg); - --tw-fade-clear-r: var(--fade-clear-lg); +.fade-clear-bottom-3xl { + --tw-fade-clear-bottom: var(--fade-clear-3xl); } -.fade-clear-x-md { - --tw-fade-clear-l: var(--fade-clear-md); - --tw-fade-clear-r: var(--fade-clear-md); +.fade-clear-bottom-4xl { + --tw-fade-clear-bottom: var(--fade-clear-4xl); } -.fade-clear-x-sm { - --tw-fade-clear-l: var(--fade-clear-sm); - --tw-fade-clear-r: var(--fade-clear-sm); +.fade-clear-bottom-lg { + --tw-fade-clear-bottom: var(--fade-clear-lg); } -.fade-clear-x-var { - --tw-fade-clear-l: var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px))); - --tw-fade-clear-r: var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px))); +.fade-clear-bottom-md { + --tw-fade-clear-bottom: var(--fade-clear-md); } -.fade-clear-x-xl { - --tw-fade-clear-l: var(--fade-clear-xl); - --tw-fade-clear-r: var(--fade-clear-xl); +.fade-clear-bottom-sm { + --tw-fade-clear-bottom: var(--fade-clear-sm); } -.fade-clear-x-xs { - --tw-fade-clear-l: var(--fade-clear-xs); - --tw-fade-clear-r: var(--fade-clear-xs); +.fade-clear-bottom-var { + --tw-fade-clear-bottom: var(--fade-clear-bottom, var(--fade-clear-y, var(--fade-clear, 0px))); } -.fade-clear-y-2xl { - --tw-fade-clear-t: var(--fade-clear-2xl); - --tw-fade-clear-b: var(--fade-clear-2xl); +.fade-clear-bottom-xl { + --tw-fade-clear-bottom: var(--fade-clear-xl); } -.fade-clear-y-3xl { - --tw-fade-clear-t: var(--fade-clear-3xl); - --tw-fade-clear-b: var(--fade-clear-3xl); +.fade-clear-bottom-xs { + --tw-fade-clear-bottom: var(--fade-clear-xs); } -.fade-clear-y-4xl { - --tw-fade-clear-t: var(--fade-clear-4xl); - --tw-fade-clear-b: var(--fade-clear-4xl); +.fade-clear-end-2xl { + --tw-fade-clear-end: var(--fade-clear-2xl); } -.fade-clear-y-lg { - --tw-fade-clear-t: var(--fade-clear-lg); - --tw-fade-clear-b: var(--fade-clear-lg); +.fade-clear-end-3xl { + --tw-fade-clear-end: var(--fade-clear-3xl); } -.fade-clear-y-md { - --tw-fade-clear-t: var(--fade-clear-md); - --tw-fade-clear-b: var(--fade-clear-md); +.fade-clear-end-4xl { + --tw-fade-clear-end: var(--fade-clear-4xl); } -.fade-clear-y-sm { - --tw-fade-clear-t: var(--fade-clear-sm); - --tw-fade-clear-b: var(--fade-clear-sm); +.fade-clear-end-lg { + --tw-fade-clear-end: var(--fade-clear-lg); } -.fade-clear-y-var { - --tw-fade-clear-t: var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px))); - --tw-fade-clear-b: var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px))); +.fade-clear-end-md { + --tw-fade-clear-end: var(--fade-clear-md); } -.fade-clear-y-xl { - --tw-fade-clear-t: var(--fade-clear-xl); - --tw-fade-clear-b: var(--fade-clear-xl); +.fade-clear-end-sm { + --tw-fade-clear-end: var(--fade-clear-sm); } -.fade-clear-y-xs { - --tw-fade-clear-t: var(--fade-clear-xs); - --tw-fade-clear-b: var(--fade-clear-xs); +.fade-clear-end-var { + --tw-fade-clear-end: var(--fade-clear-end, var(--fade-clear-x, var(--fade-clear, 0px))); } -.fade-clear-b-2xl { - --tw-fade-clear-b: var(--fade-clear-2xl); +.fade-clear-end-xl { + --tw-fade-clear-end: var(--fade-clear-xl); } -.fade-clear-b-3xl { - --tw-fade-clear-b: var(--fade-clear-3xl); +.fade-clear-end-xs { + --tw-fade-clear-end: var(--fade-clear-xs); } -.fade-clear-b-4xl { - --tw-fade-clear-b: var(--fade-clear-4xl); +.fade-clear-lg { + --tw-fade-clear: var(--fade-clear-lg); } -.fade-clear-b-lg { - --tw-fade-clear-b: var(--fade-clear-lg); +.fade-clear-md { + --tw-fade-clear: var(--fade-clear-md); } -.fade-clear-b-md { - --tw-fade-clear-b: var(--fade-clear-md); +.fade-clear-sm { + --tw-fade-clear: var(--fade-clear-sm); } -.fade-clear-b-sm { - --tw-fade-clear-b: var(--fade-clear-sm); +.fade-clear-start-2xl { + --tw-fade-clear-start: var(--fade-clear-2xl); } -.fade-clear-b-var { - --tw-fade-clear-b: var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px))); +.fade-clear-start-3xl { + --tw-fade-clear-start: var(--fade-clear-3xl); } -.fade-clear-b-xl { - --tw-fade-clear-b: var(--fade-clear-xl); +.fade-clear-start-4xl { + --tw-fade-clear-start: var(--fade-clear-4xl); } -.fade-clear-b-xs { - --tw-fade-clear-b: var(--fade-clear-xs); +.fade-clear-start-lg { + --tw-fade-clear-start: var(--fade-clear-lg); } -.fade-clear-l-2xl { - --tw-fade-clear-l: var(--fade-clear-2xl); +.fade-clear-start-md { + --tw-fade-clear-start: var(--fade-clear-md); } -.fade-clear-l-3xl { - --tw-fade-clear-l: var(--fade-clear-3xl); +.fade-clear-start-sm { + --tw-fade-clear-start: var(--fade-clear-sm); } -.fade-clear-l-4xl { - --tw-fade-clear-l: var(--fade-clear-4xl); +.fade-clear-start-var { + --tw-fade-clear-start: var(--fade-clear-start, var(--fade-clear-x, var(--fade-clear, 0px))); } -.fade-clear-l-lg { - --tw-fade-clear-l: var(--fade-clear-lg); +.fade-clear-start-xl { + --tw-fade-clear-start: var(--fade-clear-xl); } -.fade-clear-l-md { - --tw-fade-clear-l: var(--fade-clear-md); +.fade-clear-start-xs { + --tw-fade-clear-start: var(--fade-clear-xs); } -.fade-clear-l-sm { - --tw-fade-clear-l: var(--fade-clear-sm); +.fade-clear-top-2xl { + --tw-fade-clear-top: var(--fade-clear-2xl); } -.fade-clear-l-var { - --tw-fade-clear-l: var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px))); +.fade-clear-top-3xl { + --tw-fade-clear-top: var(--fade-clear-3xl); } -.fade-clear-l-xl { - --tw-fade-clear-l: var(--fade-clear-xl); +.fade-clear-top-4xl { + --tw-fade-clear-top: var(--fade-clear-4xl); } -.fade-clear-l-xs { - --tw-fade-clear-l: var(--fade-clear-xs); +.fade-clear-top-lg { + --tw-fade-clear-top: var(--fade-clear-lg); } -.fade-clear-r-2xl { - --tw-fade-clear-r: var(--fade-clear-2xl); +.fade-clear-top-md { + --tw-fade-clear-top: var(--fade-clear-md); } -.fade-clear-r-3xl { - --tw-fade-clear-r: var(--fade-clear-3xl); +.fade-clear-top-sm { + --tw-fade-clear-top: var(--fade-clear-sm); } -.fade-clear-r-4xl { - --tw-fade-clear-r: var(--fade-clear-4xl); +.fade-clear-top-var { + --tw-fade-clear-top: var(--fade-clear-top, var(--fade-clear-y, var(--fade-clear, 0px))); } -.fade-clear-r-lg { - --tw-fade-clear-r: var(--fade-clear-lg); +.fade-clear-top-xl { + --tw-fade-clear-top: var(--fade-clear-xl); } -.fade-clear-r-md { - --tw-fade-clear-r: var(--fade-clear-md); +.fade-clear-top-xs { + --tw-fade-clear-top: var(--fade-clear-xs); } -.fade-clear-r-sm { - --tw-fade-clear-r: var(--fade-clear-sm); +.fade-clear-var { + --tw-fade-clear: var(--fade-clear, 0px); } -.fade-clear-r-var { - --tw-fade-clear-r: var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px))); +.fade-clear-x-2xl { + --tw-fade-clear-x: var(--fade-clear-2xl); } -.fade-clear-r-xl { - --tw-fade-clear-r: var(--fade-clear-xl); +.fade-clear-x-3xl { + --tw-fade-clear-x: var(--fade-clear-3xl); } -.fade-clear-r-xs { - --tw-fade-clear-r: var(--fade-clear-xs); +.fade-clear-x-4xl { + --tw-fade-clear-x: var(--fade-clear-4xl); } -.fade-clear-t-2xl { - --tw-fade-clear-t: var(--fade-clear-2xl); +.fade-clear-x-lg { + --tw-fade-clear-x: var(--fade-clear-lg); } -.fade-clear-t-3xl { - --tw-fade-clear-t: var(--fade-clear-3xl); +.fade-clear-x-md { + --tw-fade-clear-x: var(--fade-clear-md); } -.fade-clear-t-4xl { - --tw-fade-clear-t: var(--fade-clear-4xl); +.fade-clear-x-sm { + --tw-fade-clear-x: var(--fade-clear-sm); } -.fade-clear-t-lg { - --tw-fade-clear-t: var(--fade-clear-lg); +.fade-clear-x-var { + --tw-fade-clear-x: var(--fade-clear-x, var(--fade-clear, 0px)); } -.fade-clear-t-md { - --tw-fade-clear-t: var(--fade-clear-md); +.fade-clear-x-xl { + --tw-fade-clear-x: var(--fade-clear-xl); } -.fade-clear-t-sm { - --tw-fade-clear-t: var(--fade-clear-sm); +.fade-clear-x-xs { + --tw-fade-clear-x: var(--fade-clear-xs); } -.fade-clear-t-var { - --tw-fade-clear-t: var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px))); +.fade-clear-xl { + --tw-fade-clear: var(--fade-clear-xl); } -.fade-clear-t-xl { - --tw-fade-clear-t: var(--fade-clear-xl); +.fade-clear-xs { + --tw-fade-clear: var(--fade-clear-xs); } -.fade-clear-t-xs { - --tw-fade-clear-t: var(--fade-clear-xs); +.fade-clear-y-2xl { + --tw-fade-clear-y: var(--fade-clear-2xl); } -.fade-range-2xl { - --tw-fade-range: var(--fade-range-2xl); +.fade-clear-y-3xl { + --tw-fade-clear-y: var(--fade-clear-3xl); } -.fade-range-3xl { - --tw-fade-range: var(--fade-range-3xl); +.fade-clear-y-4xl { + --tw-fade-clear-y: var(--fade-clear-4xl); } -.fade-range-4xl { - --tw-fade-range: var(--fade-range-4xl); +.fade-clear-y-lg { + --tw-fade-clear-y: var(--fade-clear-lg); } -.fade-range-lg { - --tw-fade-range: var(--fade-range-lg); +.fade-clear-y-md { + --tw-fade-clear-y: var(--fade-clear-md); } -.fade-range-md { - --tw-fade-range: var(--fade-range-md); +.fade-clear-y-sm { + --tw-fade-clear-y: var(--fade-clear-sm); } -.fade-range-sm { - --tw-fade-range: var(--fade-range-sm); +.fade-clear-y-var { + --tw-fade-clear-y: var(--fade-clear-y, var(--fade-clear, 0px)); } -.fade-range-xl { - --tw-fade-range: var(--fade-range-xl); +.fade-clear-y-xl { + --tw-fade-clear-y: var(--fade-clear-xl); } -.fade-range-xs { - --tw-fade-range: var(--fade-range-xs); +.fade-clear-y-xs { + --tw-fade-clear-y: var(--fade-clear-xs); } .fade-size-2xl { --tw-fade-size: var(--fade-size-2xl); @@ -766,53 +199,53 @@ .fade-size-4xl { --tw-fade-size: var(--fade-size-4xl); } -.fade-size-b-2xl { - --tw-fade-size-b: var(--fade-size-2xl); +.fade-size-bottom-2xl { + --tw-fade-size-bottom: var(--fade-size-2xl); } -.fade-size-b-3xl { - --tw-fade-size-b: var(--fade-size-3xl); +.fade-size-bottom-3xl { + --tw-fade-size-bottom: var(--fade-size-3xl); } -.fade-size-b-4xl { - --tw-fade-size-b: var(--fade-size-4xl); +.fade-size-bottom-4xl { + --tw-fade-size-bottom: var(--fade-size-4xl); } -.fade-size-b-lg { - --tw-fade-size-b: var(--fade-size-lg); +.fade-size-bottom-lg { + --tw-fade-size-bottom: var(--fade-size-lg); } -.fade-size-b-md { - --tw-fade-size-b: var(--fade-size-md); +.fade-size-bottom-md { + --tw-fade-size-bottom: var(--fade-size-md); } -.fade-size-b-sm { - --tw-fade-size-b: var(--fade-size-sm); +.fade-size-bottom-sm { + --tw-fade-size-bottom: var(--fade-size-sm); } -.fade-size-b-xl { - --tw-fade-size-b: var(--fade-size-xl); +.fade-size-bottom-xl { + --tw-fade-size-bottom: var(--fade-size-xl); } -.fade-size-b-xs { - --tw-fade-size-b: var(--fade-size-xs); +.fade-size-bottom-xs { + --tw-fade-size-bottom: var(--fade-size-xs); } -.fade-size-l-2xl { - --tw-fade-size-l: var(--fade-size-2xl); +.fade-size-end-2xl { + --tw-fade-size-end: var(--fade-size-2xl); } -.fade-size-l-3xl { - --tw-fade-size-l: var(--fade-size-3xl); +.fade-size-end-3xl { + --tw-fade-size-end: var(--fade-size-3xl); } -.fade-size-l-4xl { - --tw-fade-size-l: var(--fade-size-4xl); +.fade-size-end-4xl { + --tw-fade-size-end: var(--fade-size-4xl); } -.fade-size-l-lg { - --tw-fade-size-l: var(--fade-size-lg); +.fade-size-end-lg { + --tw-fade-size-end: var(--fade-size-lg); } -.fade-size-l-md { - --tw-fade-size-l: var(--fade-size-md); +.fade-size-end-md { + --tw-fade-size-end: var(--fade-size-md); } -.fade-size-l-sm { - --tw-fade-size-l: var(--fade-size-sm); +.fade-size-end-sm { + --tw-fade-size-end: var(--fade-size-sm); } -.fade-size-l-xl { - --tw-fade-size-l: var(--fade-size-xl); +.fade-size-end-xl { + --tw-fade-size-end: var(--fade-size-xl); } -.fade-size-l-xs { - --tw-fade-size-l: var(--fade-size-xs); +.fade-size-end-xs { + --tw-fade-size-end: var(--fade-size-xs); } .fade-size-lg { --tw-fade-size: var(--fade-size-lg); @@ -820,56 +253,56 @@ .fade-size-md { --tw-fade-size: var(--fade-size-md); } -.fade-size-r-2xl { - --tw-fade-size-r: var(--fade-size-2xl); +.fade-size-sm { + --tw-fade-size: var(--fade-size-sm); } -.fade-size-r-3xl { - --tw-fade-size-r: var(--fade-size-3xl); +.fade-size-start-2xl { + --tw-fade-size-start: var(--fade-size-2xl); } -.fade-size-r-4xl { - --tw-fade-size-r: var(--fade-size-4xl); +.fade-size-start-3xl { + --tw-fade-size-start: var(--fade-size-3xl); } -.fade-size-r-lg { - --tw-fade-size-r: var(--fade-size-lg); +.fade-size-start-4xl { + --tw-fade-size-start: var(--fade-size-4xl); } -.fade-size-r-md { - --tw-fade-size-r: var(--fade-size-md); +.fade-size-start-lg { + --tw-fade-size-start: var(--fade-size-lg); } -.fade-size-r-sm { - --tw-fade-size-r: var(--fade-size-sm); +.fade-size-start-md { + --tw-fade-size-start: var(--fade-size-md); } -.fade-size-r-xl { - --tw-fade-size-r: var(--fade-size-xl); +.fade-size-start-sm { + --tw-fade-size-start: var(--fade-size-sm); } -.fade-size-r-xs { - --tw-fade-size-r: var(--fade-size-xs); +.fade-size-start-xl { + --tw-fade-size-start: var(--fade-size-xl); } -.fade-size-sm { - --tw-fade-size: var(--fade-size-sm); +.fade-size-start-xs { + --tw-fade-size-start: var(--fade-size-xs); } -.fade-size-t-2xl { - --tw-fade-size-t: var(--fade-size-2xl); +.fade-size-top-2xl { + --tw-fade-size-top: var(--fade-size-2xl); } -.fade-size-t-3xl { - --tw-fade-size-t: var(--fade-size-3xl); +.fade-size-top-3xl { + --tw-fade-size-top: var(--fade-size-3xl); } -.fade-size-t-4xl { - --tw-fade-size-t: var(--fade-size-4xl); +.fade-size-top-4xl { + --tw-fade-size-top: var(--fade-size-4xl); } -.fade-size-t-lg { - --tw-fade-size-t: var(--fade-size-lg); +.fade-size-top-lg { + --tw-fade-size-top: var(--fade-size-lg); } -.fade-size-t-md { - --tw-fade-size-t: var(--fade-size-md); +.fade-size-top-md { + --tw-fade-size-top: var(--fade-size-md); } -.fade-size-t-sm { - --tw-fade-size-t: var(--fade-size-sm); +.fade-size-top-sm { + --tw-fade-size-top: var(--fade-size-sm); } -.fade-size-t-xl { - --tw-fade-size-t: var(--fade-size-xl); +.fade-size-top-xl { + --tw-fade-size-top: var(--fade-size-xl); } -.fade-size-t-xs { - --tw-fade-size-t: var(--fade-size-xs); +.fade-size-top-xs { + --tw-fade-size-top: var(--fade-size-xs); } .fade-size-x-2xl { --tw-fade-size-x: var(--fade-size-2xl); @@ -925,6 +358,30 @@ .fade-size-y-xs { --tw-fade-size-y: var(--fade-size-xs); } +.fade-travel-2xl { + --tw-fade-travel: var(--fade-travel-2xl); +} +.fade-travel-3xl { + --tw-fade-travel: var(--fade-travel-3xl); +} +.fade-travel-4xl { + --tw-fade-travel: var(--fade-travel-4xl); +} +.fade-travel-lg { + --tw-fade-travel: var(--fade-travel-lg); +} +.fade-travel-md { + --tw-fade-travel: var(--fade-travel-md); +} +.fade-travel-sm { + --tw-fade-travel: var(--fade-travel-sm); +} +.fade-travel-xl { + --tw-fade-travel: var(--fade-travel-xl); +} +.fade-travel-xs { + --tw-fade-travel: var(--fade-travel-xs); +} @property --tw-fade-t { syntax: ""; inherits: false; @@ -957,39 +414,51 @@ syntax: "*"; inherits: false; } -@property --tw-fade-size-t { +@property --tw-fade-size-top { + syntax: "*"; + inherits: false; +} +@property --tw-fade-size-bottom { + syntax: "*"; + inherits: false; +} +@property --tw-fade-size-start { + syntax: "*"; + inherits: false; +} +@property --tw-fade-size-end { syntax: "*"; inherits: false; } -@property --tw-fade-size-b { +@property --tw-fade-travel { syntax: "*"; inherits: false; } -@property --tw-fade-size-l { +@property --tw-fade-clear { syntax: "*"; inherits: false; } -@property --tw-fade-size-r { +@property --tw-fade-clear-y { syntax: "*"; inherits: false; } -@property --tw-fade-range { +@property --tw-fade-clear-x { syntax: "*"; inherits: false; } -@property --tw-fade-clear-t { +@property --tw-fade-clear-top { syntax: "*"; inherits: false; } -@property --tw-fade-clear-b { +@property --tw-fade-clear-bottom { syntax: "*"; inherits: false; } -@property --tw-fade-clear-l { +@property --tw-fade-clear-start { syntax: "*"; inherits: false; } -@property --tw-fade-clear-r { +@property --tw-fade-clear-end { syntax: "*"; inherits: false; } @@ -1009,6 +478,70 @@ syntax: "*"; inherits: false; } +@property --tw-fade-ltr-start-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-animation-range { + syntax: "*"; + inherits: false; +} :root, :host { --fade-size-xs: calc(var(--spacing, 0.25rem) * 6); --fade-size-sm: calc(var(--spacing, 0.25rem) * 8); @@ -1018,14 +551,14 @@ --fade-size-2xl: calc(var(--spacing, 0.25rem) * 24); --fade-size-3xl: calc(var(--spacing, 0.25rem) * 32); --fade-size-4xl: calc(var(--spacing, 0.25rem) * 40); - --fade-range-xs: calc(var(--spacing, 0.25rem) * 6); - --fade-range-sm: calc(var(--spacing, 0.25rem) * 8); - --fade-range-md: calc(var(--spacing, 0.25rem) * 12); - --fade-range-lg: calc(var(--spacing, 0.25rem) * 16); - --fade-range-xl: calc(var(--spacing, 0.25rem) * 20); - --fade-range-2xl: calc(var(--spacing, 0.25rem) * 24); - --fade-range-3xl: calc(var(--spacing, 0.25rem) * 32); - --fade-range-4xl: calc(var(--spacing, 0.25rem) * 40); + --fade-travel-xs: calc(var(--spacing, 0.25rem) * 6); + --fade-travel-sm: calc(var(--spacing, 0.25rem) * 8); + --fade-travel-md: calc(var(--spacing, 0.25rem) * 12); + --fade-travel-lg: calc(var(--spacing, 0.25rem) * 16); + --fade-travel-xl: calc(var(--spacing, 0.25rem) * 20); + --fade-travel-2xl: calc(var(--spacing, 0.25rem) * 24); + --fade-travel-3xl: calc(var(--spacing, 0.25rem) * 32); + --fade-travel-4xl: calc(var(--spacing, 0.25rem) * 40); --fade-clear-xs: calc(var(--spacing, 0.25rem) * 6); --fade-clear-sm: calc(var(--spacing, 0.25rem) * 8); --fade-clear-md: calc(var(--spacing, 0.25rem) * 12); @@ -1035,7 +568,7 @@ --fade-clear-3xl: calc(var(--spacing, 0.25rem) * 32); --fade-clear-4xl: calc(var(--spacing, 0.25rem) * 40); } -@keyframes tw-fade-reveal-t { +@keyframes tw-fade-travel-t { from { --tw-fade-t: 0; } @@ -1043,7 +576,7 @@ --tw-fade-t: 1; } } -@keyframes tw-fade-reveal-b { +@keyframes tw-fade-travel-b { from { --tw-fade-b: 1; } @@ -1051,7 +584,7 @@ --tw-fade-b: 0; } } -@keyframes tw-fade-reveal-l { +@keyframes tw-fade-travel-l { from { --tw-fade-l: 0; } @@ -1059,7 +592,7 @@ --tw-fade-l: 1; } } -@keyframes tw-fade-reveal-r { +@keyframes tw-fade-travel-r { from { --tw-fade-r: 1; } @@ -1067,10 +600,263 @@ --tw-fade-r: 0; } } +@keyframes tw-fade-travel-r-leading { + from { + --tw-fade-r: 0; + } + to { + --tw-fade-r: 1; + } +} +@keyframes tw-fade-travel-l-trailing { + from { + --tw-fade-l: 1; + } + to { + --tw-fade-l: 0; + } +} @layer utilities { - .fade-t, .fade-b, .fade-l, .fade-r, .fade-y, .fade-x, .fade-xy { - --tw-fade-size-default: var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)); - --tw-fade-range-active: var(--tw-fade-range, var(--fade-range-md, calc(var(--spacing, 0.25rem) * 12))); + .fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end { + --tw-fade-size-default: min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12))); + --tw-fade-travel-active: var(--tw-fade-travel, var(--fade-travel-sm, calc(var(--spacing, 0.25rem) * 8))); + --tw-fade-identity-mask: linear-gradient(#000, #000); + --tw-fade-edge-size-t: var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-b: var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-l: var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-r: var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-clear-t: var(--tw-fade-clear-top, var(--tw-fade-clear-y, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-b: var(--tw-fade-clear-bottom, var(--tw-fade-clear-y, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-l: var(--tw-fade-clear-start, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-r: var(--tw-fade-clear-end, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); + --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); + --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); + --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); + --tw-fade-onset: 8; + --tw-fade-alpha-t: min(1, calc(var(--tw-fade-t) * var(--tw-fade-onset))); + --tw-fade-alpha-b: min(1, calc(var(--tw-fade-b) * var(--tw-fade-onset))); + --tw-fade-alpha-l: min(1, calc(var(--tw-fade-l) * var(--tw-fade-onset))); + --tw-fade-alpha-r: min(1, calc(var(--tw-fade-r) * var(--tw-fade-onset))); + --tw-fade-gradient-t: linear-gradient( + to bottom, + #000 0 var(--tw-fade-edge-clear-t), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t))) var(--tw-fade-edge-clear-t), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.983)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.933)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.854)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.75)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.629)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.5)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.371)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.25)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.146)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.067)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.017)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) + ); + --tw-fade-gradient-b: linear-gradient( + to top, + #000 0 var(--tw-fade-edge-clear-b), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b))) var(--tw-fade-edge-clear-b), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.983)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.933)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.854)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.75)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.629)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.5)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.371)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.25)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.146)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.067)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.017)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) + ); + --tw-fade-gradient-l: linear-gradient( + to right, + #000 0 var(--tw-fade-edge-clear-l), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l))) var(--tw-fade-edge-clear-l), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.983)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.933)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.854)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.75)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.629)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.5)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.371)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.25)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.146)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.067)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.017)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) + ); + --tw-fade-gradient-r: linear-gradient( + to left, + #000 0 var(--tw-fade-edge-clear-r), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r))) var(--tw-fade-edge-clear-r), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.983)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.933)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.854)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.75)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.629)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.5)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.371)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.25)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.146)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.067)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.017)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) + ); + --tw-fade-mask-l: var(--tw-fade-ltr-start-layer, var(--tw-fade-identity-mask)); + --tw-fade-mask-r: var(--tw-fade-ltr-end-layer, var(--tw-fade-identity-mask)); + --tw-fade-animation-l: var(--tw-fade-ltr-start-animation, none); + --tw-fade-animation-r: var(--tw-fade-ltr-end-animation, none); + --tw-fade-timeline-l: var(--tw-fade-ltr-start-timeline, none); + --tw-fade-timeline-r: var(--tw-fade-ltr-end-timeline, none); + --tw-fade-animation-range-l: var(--tw-fade-ltr-start-animation-range, normal); + --tw-fade-animation-range-r: var(--tw-fade-ltr-end-animation-range, normal); + --tw-fade-animation-t: none; + --tw-fade-animation-b: none; + --tw-fade-timeline-t: none; + --tw-fade-timeline-b: none; + --tw-fade-animation-range-t: normal; + --tw-fade-animation-range-b: normal; + mask-image: var(--tw-fade-mask-t, var(--tw-fade-identity-mask)), var(--tw-fade-mask-b, var(--tw-fade-identity-mask)), var(--tw-fade-mask-l, var(--tw-fade-identity-mask)), var(--tw-fade-mask-r, var(--tw-fade-identity-mask)); + mask-composite: intersect; + mask-repeat: no-repeat; + mask-size: 100% 100%; + } + .fade:where(:dir(rtl)), .fade-y:where(:dir(rtl)), .fade-top:where(:dir(rtl)), .fade-bottom:where(:dir(rtl)), .fade-x:where(:dir(rtl)), .fade-start:where(:dir(rtl)), .fade-end:where(:dir(rtl)) { + --tw-fade-edge-size-l: var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-r: var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-clear-l: var(--tw-fade-clear-end, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-r: var(--tw-fade-clear-start, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-mask-l: var(--tw-fade-rtl-end-layer, var(--tw-fade-identity-mask)); + --tw-fade-mask-r: var(--tw-fade-rtl-start-layer, var(--tw-fade-identity-mask)); + --tw-fade-animation-l: var(--tw-fade-rtl-end-animation, none); + --tw-fade-animation-r: var(--tw-fade-rtl-start-animation, none); + --tw-fade-timeline-l: var(--tw-fade-rtl-end-timeline, none); + --tw-fade-timeline-r: var(--tw-fade-rtl-start-timeline, none); + --tw-fade-animation-range-l: var(--tw-fade-rtl-end-animation-range, normal); + --tw-fade-animation-range-r: var(--tw-fade-rtl-start-animation-range, normal); + } + .fade, .fade-y, .fade-top { + --tw-fade-mask-t: var(--tw-fade-gradient-t); + --tw-fade-animation-t: tw-fade-travel-t; + --tw-fade-timeline-t: scroll(self y); + --tw-fade-animation-range-t: 0 var(--tw-fade-travel-active); + } + .fade, .fade-y, .fade-bottom { + --tw-fade-mask-b: var(--tw-fade-gradient-b); + --tw-fade-animation-b: tw-fade-travel-b; + --tw-fade-timeline-b: scroll(self y); + --tw-fade-animation-range-b: calc(100% - var(--tw-fade-travel-active)) 100%; + } + .fade, .fade-x, .fade-start { + --tw-fade-ltr-start-layer: var(--tw-fade-gradient-l); + --tw-fade-rtl-start-layer: var(--tw-fade-gradient-r); + --tw-fade-ltr-start-animation: tw-fade-travel-l; + --tw-fade-rtl-start-animation: tw-fade-travel-r-leading; + --tw-fade-ltr-start-timeline: scroll(self inline); + --tw-fade-rtl-start-timeline: scroll(self inline); + --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-travel-active); + --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-travel-active); + } + .fade, .fade-x, .fade-end { + --tw-fade-ltr-end-layer: var(--tw-fade-gradient-r); + --tw-fade-rtl-end-layer: var(--tw-fade-gradient-l); + --tw-fade-ltr-end-animation: tw-fade-travel-r; + --tw-fade-rtl-end-animation: tw-fade-travel-l-trailing; + --tw-fade-ltr-end-timeline: scroll(self inline); + --tw-fade-rtl-end-timeline: scroll(self inline); + --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%; + --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%; + } + @supports (animation-timeline: scroll()) { + .fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end { + animation: var(--tw-fade-animation-t, none) auto linear both, var(--tw-fade-animation-b, none) auto linear both, var(--tw-fade-animation-l, none) auto linear both, var(--tw-fade-animation-r, none) auto linear both; + animation-timeline: var(--tw-fade-timeline-t, none), var(--tw-fade-timeline-b, none), var(--tw-fade-timeline-l, none), var(--tw-fade-timeline-r, none); + animation-range: var(--tw-fade-animation-range-t, normal), var(--tw-fade-animation-range-b, normal), var(--tw-fade-animation-range-l, normal), var(--tw-fade-animation-range-r, normal); + } + } + @supports not (animation-timeline: scroll()) { + .fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end { + --tw-fade-t: 1; + --tw-fade-b: 1; + --tw-fade-l: 1; + --tw-fade-r: 1; + } + } + .fade-none { + --tw-fade-t: 0 !important; + --tw-fade-b: 0 !important; + --tw-fade-l: 0 !important; + --tw-fade-r: 0 !important; + } + .fade-none-y { + --tw-fade-t: 0 !important; + --tw-fade-b: 0 !important; + } + .fade-none-x { + --tw-fade-l: 0 !important; + --tw-fade-r: 0 !important; + } + .fade-always { + --tw-fade-t: 1 !important; + --tw-fade-b: 1 !important; + --tw-fade-l: 1 !important; + --tw-fade-r: 1 !important; + } + .fade-always-y { + --tw-fade-t: 1 !important; + --tw-fade-b: 1 !important; + } + .fade-always-x { + --tw-fade-l: 1 !important; + --tw-fade-r: 1 !important; } } @layer properties { @@ -1083,19 +869,38 @@ --tw-fade-size: initial; --tw-fade-size-y: initial; --tw-fade-size-x: initial; - --tw-fade-size-t: initial; - --tw-fade-size-b: initial; - --tw-fade-size-l: initial; - --tw-fade-size-r: initial; - --tw-fade-range: initial; - --tw-fade-clear-t: initial; - --tw-fade-clear-b: initial; - --tw-fade-clear-l: initial; - --tw-fade-clear-r: initial; + --tw-fade-size-top: initial; + --tw-fade-size-bottom: initial; + --tw-fade-size-start: initial; + --tw-fade-size-end: initial; + --tw-fade-travel: initial; + --tw-fade-clear: initial; + --tw-fade-clear-y: initial; + --tw-fade-clear-x: initial; + --tw-fade-clear-top: initial; + --tw-fade-clear-bottom: initial; + --tw-fade-clear-start: initial; + --tw-fade-clear-end: initial; --tw-fade-mask-t: initial; --tw-fade-mask-b: initial; --tw-fade-mask-l: initial; --tw-fade-mask-r: initial; + --tw-fade-ltr-start-layer: initial; + --tw-fade-ltr-end-layer: initial; + --tw-fade-rtl-start-layer: initial; + --tw-fade-rtl-end-layer: initial; + --tw-fade-ltr-start-animation: initial; + --tw-fade-ltr-end-animation: initial; + --tw-fade-rtl-start-animation: initial; + --tw-fade-rtl-end-animation: initial; + --tw-fade-ltr-start-timeline: initial; + --tw-fade-ltr-end-timeline: initial; + --tw-fade-rtl-start-timeline: initial; + --tw-fade-rtl-end-timeline: initial; + --tw-fade-ltr-start-animation-range: initial; + --tw-fade-ltr-end-animation-range: initial; + --tw-fade-rtl-start-animation-range: initial; + --tw-fade-rtl-end-animation-range: initial; } } } diff --git a/package-lock.json b/package-lock.json index 6ce7f57..a5f82c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "tw-fade", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tw-fade", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "devDependencies": { "@tailwindcss/cli": "^4.0.0", "playwright": "^1.61.0", - "tailwindcss": "^4.3.1" + "tailwindcss": "^4.3.1", + "tw-shimmer": "^0.4.11" }, "peerDependencies": { "tailwindcss": ">=4.0.0" @@ -1224,6 +1225,16 @@ "engines": { "node": ">=8.0" } + }, + "node_modules/tw-shimmer": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/tw-shimmer/-/tw-shimmer-0.4.11.tgz", + "integrity": "sha512-pTpGJzp3xaCPO87WeHETngmZHJYvygiSTt4jqzh2oR3DWBoeudi/ANB304zks9+Cm2vQ1ai3w9fetviYdqY8HQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=4.0.0-0" + } } } } diff --git a/package.json b/package.json index 558f8a3..65c5c67 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tw-fade", - "version": "0.6.0", - "description": "Elegant, CSS-driven scroll-edge fade masking for Tailwind CSS v4. Add fade-t/b/l/r (or -x/-y/-xy) to any scroll container to fade content into its surface at the edges — scroll-gated with zero JavaScript.", + "version": "0.7.0", + "description": "Elegant, CSS-driven scroll-edge fade masking for Tailwind CSS v4. Add fade-y, fade-x, fade-start, or fade-end to any scroll container to fade content into its surface at the edges — scroll-gated with zero JavaScript.", "keywords": [ "tailwindcss", "tailwind-plugin", @@ -12,7 +12,11 @@ "scroll-driven-animations", "scrim", "edge-fade", - "overflow" + "overflow", + "rtl", + "direction-aware", + "logical-properties", + "bidi" ], "license": "MIT", "author": "Pete Petrash", @@ -35,17 +39,20 @@ "src", "dist", "README.md", + "MIGRATING.md", + "CHANGELOG.md", "LICENSE" ], "scripts": { "build": "node scripts/build-css.mjs", "build:demo": "tailwindcss -i demo/input.css -o demo/styles.css", + "debug": "node scripts/build-debug.mjs", "export:demo": "node scripts/export-demo-static.mjs", "ship-demo": "node scripts/ship-demo.mjs", "generate:seo": "node scripts/generate-seo-assets.mjs", "release": "node scripts/release.mjs", "test": "node --test", - "verify": "node build/verify.mjs && node build/verify-dist.mjs && node build/verify-fade.mjs && node build/verify-demo-assets.mjs && node build/verify-page.mjs && node build/verify-demo-runtime.mjs && node build/verify-favicon.mjs && node build/verify-seo.mjs", + "verify": "node build/verify.mjs && node build/verify-horizontal-rtl.mjs && node build/verify-dist.mjs && node build/verify-fade.mjs && node build/verify-demo-assets.mjs && node build/verify-page.mjs && node build/verify-demo-runtime.mjs && node build/verify-favicon.mjs && node build/verify-seo.mjs", "prepublishOnly": "node scripts/build-css.mjs && node --test" }, "peerDependencies": { @@ -59,6 +66,7 @@ "devDependencies": { "@tailwindcss/cli": "^4.0.0", "playwright": "^1.61.0", - "tailwindcss": "^4.3.1" + "tailwindcss": "^4.3.1", + "tw-shimmer": "^0.4.11" } } diff --git a/scripts/build-css.mjs b/scripts/build-css.mjs index 856528a..784c0a1 100644 --- a/scripts/build-css.mjs +++ b/scripts/build-css.mjs @@ -37,12 +37,12 @@ const src = path.join(root, 'src', 'tw-fade.css') const outFile = path.join(root, 'dist', 'tw-fade.css') // Every class the framework-free build should contain. Arbitrary values -// (fade-size-[6rem], fade-range-[80px], fade-clear-t-[56px]) are intentionally +// (fade-size-[6rem], fade-travel-[80px], fade-clear-top-[56px]) are intentionally // absent — those require Tailwind's JIT and belong to the v4 source path, not // the prebuilt drop-in. const SCALE = 'xs,sm,md,lg,xl,2xl,3xl,4xl' const CLASSES = - `fade-{t,b,l,r,x,y,xy} fade-static fade-size-{${SCALE}} fade-size-{t,b,l,r,x,y}-{${SCALE}} fade-range-{${SCALE}} fade-clear-{t,b,l,r,x,y,xy}-{${SCALE},var}` + `fade fade-{x,y,top,bottom,start,end} fade-none fade-none-{x,y} fade-always fade-always-{x,y} fade-size-{${SCALE}} fade-size-{x,y,top,bottom,start,end}-{${SCALE}} fade-travel-{${SCALE}} fade-clear-{${SCALE},var} fade-clear-{x,y,top,bottom,start,end}-{${SCALE},var}` const BANNER = '/*! tw-fade — framework-free build for plain HTML / CDN.\n' + diff --git a/scripts/build-debug.mjs b/scripts/build-debug.mjs new file mode 100644 index 0000000..1860a17 --- /dev/null +++ b/scripts/build-debug.mjs @@ -0,0 +1,111 @@ +/** + * build-debug.mjs — assemble debug/index.html, a self-contained pressure-test + * harness for the fade engine. + * + * node scripts/build-debug.mjs + * + * It inlines three sources into one standalone file (open via file:// or any + * static server, no build step at view time): + * 1. the compiled plugin CSS (compileCss() — the SAME output dist/ ships, so + * the harness always reflects the real engine, every named utility present); + * 2. scripts/debug/harness.css — UI chrome; + * 3. scripts/debug/harness.js — scenario config + controls + scroll sync. + * + * The harness drives the engine through its public custom properties, so this + * generator never needs to know the gradient internals — it just ships the + * current plugin CSS verbatim. + */ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { compileCss } from './build-css.mjs' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, '..') +const debugDir = path.join(root, 'debug') +const outFile = path.join(debugDir, 'index.html') + +const pluginCss = compileCss() +const harnessCss = fs.readFileSync(path.join(__dirname, 'debug', 'harness.css'), 'utf8') +const harnessJs = fs.readFileSync(path.join(__dirname, 'debug', 'harness.js'), 'utf8') + +const ctrl = (label, valId, inner) => + `
${inner}
` + +const panel = ` +
+
+

tw-fade · pressure test

+ every sample is pinned to the same scroll offset — scrub to compare frame-for-frame + + +
+
+ ${ctrl('Fade size', 'sizeVal', '')} + ${ctrl('Travel (band open)', 'travelVal', '')} + ${ctrl('Onset (edge speed)', 'onsetVal', '')} + ${ctrl('Clear', 'clearVal', '')} +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ ${ctrl('Content', null, '')} + ${ctrl('Background', null, '')} + ${ctrl('Container shape', null, '')} +
+ + +
+
+

+ onset 1 reproduces the old coupled hard-clip (alpha = t); 8 is the decoupled default. + static fallback forces fade-always (the no-scroll-driven-animation path). Set scroll to 8px from start to inspect the onset edge-coverage window; switch to px from end to probe the trailing edge (fade-bottom / fade-end — a separate range). +

+
` + +const html = ` + + + + +tw-fade · pressure test + + + +${panel} +
+ + + +` + +fs.mkdirSync(debugDir, { recursive: true }) +fs.writeFileSync(outFile, html) +console.log(`wrote ${path.relative(root, outFile)} (${(html.length / 1024).toFixed(1)} KB)`) diff --git a/scripts/debug/README.md b/scripts/debug/README.md new file mode 100644 index 0000000..f03231c --- /dev/null +++ b/scripts/debug/README.md @@ -0,0 +1,77 @@ +# tw-fade debug harness + +An interactive pressure-test bench for the fade engine. It renders the plugin +across every direction utility, a battery of real-world / edge-case layouts, and +four parameter sweeps — all pinned to the **same scroll offset** so any bug is a +side-by-side visual diff. + +## Run + +```bash +npm run debug # → debug/index.html (self-contained, gitignored) +``` + +Open `debug/index.html` in a **Chromium-based browser** (needs scroll-driven +animations + `corner-shape`; Chrome/Edge ≥ 115-ish, full squircle support ≥ 149). +The file inlines the *compiled* plugin CSS — the same output `dist/` ships — so the +harness always reflects the real engine. Re-run `npm run debug` after editing +`src/tw-fade.css`. + +## What it exercises + +- **Directions** — all seven utilities (`fade`, `fade-y`, `fade-x`, `fade-top`, + `fade-bottom`, `fade-start`, `fade-end`) on identical content. +- **Real-world & edge cases** — short (non-overflowing) content, sticky-header + occlusion, padding lead-in, scroll-snap, nested fade scrollers, image/code/dense + content, tiny containers, thick borders, oversized (50%) fades, per-edge sizes, + `overflow: clip` trap, and the `fade-none` / `fade-none-y` / `fade-always-x` + override paths. +- **Sweeps** — travel, onset, size, clear each across their scale at the global + scroll offset. + +## Controls + +- **Synchronized scroll** — one slider drives every scroller. Fine chips + (`0/2/4/6/8/12/24px`) plus a **px-from-start / px-from-end** toggle let you probe + the onset edge-coverage window on the leading edge *and* the separate trailing + range. `auto` oscillates. +- **size / travel / onset / clear** sliders write the public custom properties + (`--tw-fade-size`, `--tw-fade-travel`, `--tw-fade-onset`, `--tw-fade-clear`) + directly on each scroller — no class explosion, continuous values. + - **onset 1 reproduces the old coupled hard-clip** (`alpha = t`); 8 is the + decoupled default. That is the A/B. +- **static fallback** forces `fade-always`, mirroring the `@supports not + (animation-timeline: scroll())` path that pins amounts to 1. +- **RTL**, **content type**, **background** (light/dark/checker/photo — reveals the + mask silhouette), and **container shape** (rounded/squircle/sharp). +- Each card shows a live readout: scroll offset, `%`, and the resolved + `--tw-fade-{t,b,l,r}` amounts. A non-overflowing card reads `no overflow → no + fade` (the key scroll-driven invariant). + +## Architecture + +Three sources, assembled into one standalone file by `scripts/build-debug.mjs`: + +| File | Role | +| --- | --- | +| `scripts/build-debug.mjs` | generator — inlines compiled plugin CSS + the two files below, writes `debug/index.html`, emits the control-panel markup | +| `scripts/debug/harness.css` | UI chrome only (never touches the fade engine) | +| `scripts/debug/harness.js` | scenario config, controls, scroll-sync engine | + +The config props are `@property … inherits:false`, so the harness sets them on +**each `.fade*` element directly** (a parent would not cascade). + +### Add a scenario + +Append an object to `SCENARIOS.directions` / `SCENARIOS.realworld` in +`harness.js`: + +```js +{ id: 'myCase', label: 'My case', cls: 'fade-y', special: 'simple', + content: 'text', lock: { travel: 80, vars: { '--tw-fade-size-top': '64px' } }, + note: 'what to look for' } +``` + +`special` selects a builder (`simple`, `plane`, `hscroll`, `sticky`, `leadin`, +`snap`, `short`, `clip`, `nested`, `bordered`); `lock` overrides global params for +that card (`size`/`travel`/`onset`/`clear`, or arbitrary inline `vars`). diff --git a/scripts/debug/harness.css b/scripts/debug/harness.css new file mode 100644 index 0000000..2f89792 --- /dev/null +++ b/scripts/debug/harness.css @@ -0,0 +1,354 @@ +/* tw-fade debug harness — UI chrome only. The plugin CSS is inlined separately + * by scripts/build-debug.mjs. Nothing here touches the fade engine; it only + * frames the sample scrollers and the control panel. */ + +:root { + --ui-bg: #0d0f14; + --ui-panel: #151922; + --ui-panel-2: #1c2230; + --ui-line: #2a3140; + --ui-text: #e6e9ef; + --ui-dim: #9aa4b5; + --ui-accent: #6ea8fe; + --ui-accent-2: #5be0c0; + --ui-warn: #ffb454; + --stage-bg: #f4f5f7; + --stage-card: #ffffff; + --stage-text: #1b2430; + --stage-dim: #5b6675; + --stage-line: #e2e5ea; + --scroller-h: 220px; +} + +* { box-sizing: border-box; } + +html, body { margin: 0; } + +body { + background: var(--ui-bg); + color: var(--ui-text); + font: 13px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--ui-accent); } + +/* ---- Control panel ---------------------------------------------------- */ + +.panel { + position: sticky; + top: 0; + z-index: 50; + background: color-mix(in srgb, var(--ui-panel) 92%, transparent); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--ui-line); + padding: 12px 16px; +} + +.panel-title { + display: flex; + align-items: baseline; + gap: 10px; + margin: 0 0 10px; +} +.panel-title h1 { font-size: 14px; margin: 0; letter-spacing: 0.2px; } +.panel-title .sub { color: var(--ui-dim); font-size: 12px; } +.panel-title .spacer { flex: 1; } + +.controls { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + align-items: flex-end; +} + +.ctrl { display: flex; flex-direction: column; gap: 3px; min-width: 0; } +.ctrl > label { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--ui-dim); + display: flex; + justify-content: space-between; + gap: 8px; +} +.ctrl > label .val { color: var(--ui-accent-2); font-variant-numeric: tabular-nums; } + +.ctrl input[type="range"] { width: 168px; accent-color: var(--ui-accent); margin: 4px 0 0; } +.ctrl select, +.ctrl input[type="text"] { + background: var(--ui-panel-2); + color: var(--ui-text); + border: 1px solid var(--ui-line); + border-radius: 6px; + padding: 5px 8px; + font: inherit; +} + +.seg { display: inline-flex; gap: 0; border: 1px solid var(--ui-line); border-radius: 7px; overflow: hidden; } +.seg button { + background: var(--ui-panel-2); + color: var(--ui-dim); + border: 0; + border-right: 1px solid var(--ui-line); + padding: 5px 9px; + font: inherit; + cursor: pointer; +} +.seg button:last-child { border-right: 0; } +.seg button:hover { color: var(--ui-text); } +.seg button[aria-pressed="true"] { background: var(--ui-accent); color: #06121f; font-weight: 600; } + +.toggle { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--ui-panel-2); + border: 1px solid var(--ui-line); + border-radius: 7px; + padding: 5px 9px; + cursor: pointer; + user-select: none; +} +.toggle input { accent-color: var(--ui-accent); margin: 0; } +.toggle.warn[data-on="true"] { border-color: var(--ui-warn); color: var(--ui-warn); } + +.btn { + background: var(--ui-panel-2); + color: var(--ui-text); + border: 1px solid var(--ui-line); + border-radius: 7px; + padding: 6px 11px; + font: inherit; + cursor: pointer; +} +.btn:hover { border-color: var(--ui-accent); } +.btn.primary { background: var(--ui-accent); color: #06121f; border-color: transparent; font-weight: 600; } + +.scrubrow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.scrubrow .steps { display: inline-flex; gap: 4px; } +.scrubrow .steps button { + background: var(--ui-panel-2); + color: var(--ui-dim); + border: 1px solid var(--ui-line); + border-radius: 6px; + padding: 4px 7px; + font: 11px/1 ui-monospace, monospace; + cursor: pointer; +} +.scrubrow .steps button:hover { color: var(--ui-text); border-color: var(--ui-accent); } + +.hint { color: var(--ui-dim); font-size: 11px; margin: 8px 0 0; } +.hint code { color: var(--ui-accent-2); } + +/* ---- Stage (sample area) --------------------------------------------- */ + +.stage { padding: 18px 16px 64px; } + +.stage.bg-light { background: var(--stage-bg); } +.stage.bg-dark { background: #11151c; } +.stage.bg-check { + background: + repeating-conic-gradient(#dfe3ea 0 25%, #c5ccd8 0 50%) 0 0 / 22px 22px; +} +.stage.bg-photo { + background: linear-gradient(125deg, #ff9a9e, #fad0c4 30%, #a1c4fd 65%, #c2e9fb); +} + +.section-h { + display: flex; + align-items: baseline; + gap: 10px; + margin: 26px 4px 12px; + color: var(--stage-text); +} +.stage.bg-dark .section-h, .stage.bg-photo .section-h { color: #fff; } +.section-h h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 1px; margin: 0; } +.section-h .desc { font-size: 12px; color: var(--stage-dim); font-weight: 400; text-transform: none; letter-spacing: 0; } +.stage.bg-dark .section-h .desc, .stage.bg-photo .section-h .desc { color: rgba(255,255,255,0.8); } + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 14px; +} +.grid.sweep { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + +.card { + background: var(--stage-card); + border: 1px solid var(--stage-line); + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 1px 2px rgba(0,0,0,0.04); +} + +.card-head { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--stage-line); + background: #fafbfc; +} +.card-label { font-weight: 600; font-size: 12px; color: var(--stage-text); } +.card-tag { + font: 10px/1 ui-monospace, monospace; + background: #eef1f6; + color: #46506a; + padding: 3px 6px; + border-radius: 5px; +} +.card-head .readout { + margin-left: auto; + font: 10px/1 ui-monospace, monospace; + color: var(--stage-dim); + font-variant-numeric: tabular-nums; +} + +.card-note { + padding: 7px 10px; + font-size: 11px; + color: var(--stage-dim); + border-top: 1px solid var(--stage-line); + background: #fafbfc; +} + +/* The scroll container that actually carries a fade-* class. Position + * relative so the optional band overlay can pin to it. */ +.scroller { + position: relative; + height: var(--scroller-h); + overflow: auto; + background: var(--stage-card); + color: var(--stage-text); + scrollbar-width: thin; +} +.scroller.h { overflow: auto; } +.scroller.short { height: 120px; } +.scroller.tiny { height: 84px; } + +.stage.bg-dark .card, .stage.bg-dark .scroller { background: #1b2230; color: #dfe5ee; border-color: #2a3346; } +.stage.bg-dark .card-head, .stage.bg-dark .card-note { background: #161d29; border-color: #2a3346; } +.stage.bg-dark .card-label { color: #eef2f8; } + +/* ---- Sample content -------------------------------------------------- */ + +.body-pad { padding: 14px 16px; } +.body-pad p { margin: 0 0 11px; } +.body-pad h3 { margin: 0 0 8px; font-size: 14px; } + +.lead-in { padding-top: 48px; padding-bottom: 48px; } + +.minilist { padding: 8px; display: flex; flex-direction: column; gap: 8px; } +.minicard { + border: 1px solid var(--stage-line); + border-radius: 9px; + padding: 10px 12px; + background: #fbfcfe; +} +.minicard b { display: block; font-size: 12px; } +.minicard span { color: var(--stage-dim); font-size: 11px; } +.stage.bg-dark .minicard { background: #222a3a; border-color: #313b50; } + +.tilegrid { padding: 10px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } +.tile { aspect-ratio: 4 / 3; border-radius: 9px; } + +.code { margin: 0; padding: 12px 14px; font: 11.5px/1.7 ui-monospace, SFMono-Regular, monospace; white-space: pre; color: #2b3850; } +.stage.bg-dark .code { color: #c7d2e6; } + +.dense { padding: 0; } +.dense .row { + display: flex; + justify-content: space-between; + padding: 7px 12px; + border-bottom: 1px solid var(--stage-line); + font-size: 12px; +} +.dense .row span:last-child { color: var(--stage-dim); font-variant-numeric: tabular-nums; } + +/* Horizontal content */ +.scroller.h .track { display: inline-flex; gap: 10px; padding: 14px; white-space: nowrap; height: 100%; align-items: center; } +.chip { + flex: 0 0 auto; + border: 1px solid var(--stage-line); + border-radius: 999px; + padding: 8px 16px; + background: #fbfcfe; + font-size: 12px; + font-weight: 500; +} +.stage.bg-dark .chip { background: #222a3a; border-color: #313b50; } +.hcard { + flex: 0 0 180px; + height: 150px; + border-radius: 11px; + padding: 12px; + color: #fff; + display: flex; + flex-direction: column; + justify-content: flex-end; + font-weight: 600; +} + +/* 2D content for fade (all edges) */ +.plane { width: 760px; padding: 14px; } +.plane .grid2d { display: grid; grid-template-columns: repeat(8, 84px); gap: 8px; } +.plane .cell { height: 84px; border-radius: 8px; display: grid; place-items: center; color: #fff; font-weight: 600; } + +/* Sticky header inside a scroller */ +.sticky-h { + position: sticky; + top: 0; + background: color-mix(in srgb, var(--stage-card) 86%, transparent); + backdrop-filter: blur(4px); + padding: 8px 14px; + font-weight: 600; + font-size: 12px; + border-bottom: 1px solid var(--stage-line); +} +.stage.bg-dark .sticky-h { background: color-mix(in srgb, #1b2230 86%, transparent); } + +/* Nested scroller card */ +.nested-outer { padding: 14px; } +.nested-inner { + height: 150px; + overflow: auto; + border: 1px dashed var(--stage-line); + border-radius: 9px; + margin: 10px 0; + background: #fbfcfe; +} +.stage.bg-dark .nested-inner { background: #222a3a; } + +/* Scroll-snap */ +.snap { scroll-snap-type: y mandatory; } +.snap .snapitem { + scroll-snap-align: start; + height: 110px; + margin: 8px 12px; + border-radius: 10px; + display: grid; + place-items: center; + color: #fff; + font-weight: 700; + font-size: 18px; +} + +/* Container shapes (applied to .scroller via JS shape class) */ +.shape-rounded .scroller { border-radius: 14px; } +.shape-squircle .scroller { border-radius: 26px; corner-shape: superellipse(1.8); } +.shape-sharp .scroller { border-radius: 0; } +.shape-rounded .card, .shape-squircle .card { border-radius: 14px; } + +.legend { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin: 6px 4px 0; + font-size: 11px; + color: var(--stage-dim); +} +.stage.bg-dark .legend, .stage.bg-photo .legend { color: rgba(255,255,255,0.85); } +.legend .sw { display: inline-block; width: 11px; height: 11px; border-radius: 3px; vertical-align: -1px; margin-right: 5px; } diff --git a/scripts/debug/harness.js b/scripts/debug/harness.js new file mode 100644 index 0000000..4bd85e0 --- /dev/null +++ b/scripts/debug/harness.js @@ -0,0 +1,452 @@ +/* tw-fade debug harness — runtime logic. + * + * Drives the inlined plugin via the public custom properties only: + * --tw-fade-size / --tw-fade-travel / --tw-fade-onset / --tw-fade-clear + * (all @property inherits:false, so they are set on each .fade* element + * directly, never a parent). Structural scenarios + parameter sweeps are + * generated from the SCENARIOS / SWEEPS config below. The single most useful + * affordance for a scroll-driven plugin is synchronized scroll: every sample + * scroller is pinned to the same offset so you can compare them frame-for-frame. + * + * Notes baked into the controls: + * - onset = 1 reproduces the OLD coupled behavior (alpha = t); 8 is the new + * decoupled default. That IS the A/B toggle. + * - "Static fallback" forces .fade-always, mirroring the @supports-not path + * that pins t/b/l/r = 1 where scroll-driven animation is unsupported. + */ +(() => { + 'use strict'; + + const COLORS = ['#6ea8fe', '#5be0c0', '#ffb454', '#ff7eb6', '#b388ff', '#7ee787', '#f0883e', '#79c0ff']; + const grad = (i) => `linear-gradient(135deg, ${COLORS[i % COLORS.length]}, ${COLORS[(i + 3) % COLORS.length]})`; + + const state = { + size: null, // null = plugin default; else number (px) + travel: null, // null = plugin default; else number (px) + onset: 8, + clear: 0, // px + scrollPct: 0, // 0..1 + scrollPx: null, // when set, absolute px offset (overrides pct) + scrollMode: 'start', // 'start' | 'end' — which side scrollPx is measured from + rtl: false, + content: 'auto', + bg: 'light', + shape: 'rounded', + fallback: false, + }; + + let autoTimer = null; + let autoDir = 1; + + /* ---- content generators ------------------------------------------- */ + const LOREM = [ + 'Scroll-driven masks ride the container’s own scroll position—no JavaScript, no observers.', + 'The leading edge is covered the instant you scroll, while the soft band keeps easing open over the travel.', + 'Each edge is an independent linear-gradient mask layer, composited with mask-composite: intersect.', + 'Because the fade is a mask, whatever sits behind the scroller shows through the faded pixels.', + 'Try a checkerboard or photo background to see the exact silhouette the mask carves.', + 'Vertical edges stay physical; horizontal start/end edges flip with text direction.', + 'A flush, unscrolled edge shows nothing—alpha is zero until the scroll timeline advances.', + 'Crank onset down to 1 and the old hard-clip coupling returns: the edge lags the scroll.', + ]; + + function cText(n) { + let h = '

Read me while scrolling

'; + for (let i = 0; i < n; i++) h += `

${LOREM[i % LOREM.length]}

`; + return h + '
'; + } + function cCards(n) { + let h = '
'; + for (let i = 0; i < n; i++) h += `
Item ${i + 1}secondary line of supporting metadata
`; + return h + '
'; + } + function cTiles(n) { + let h = '
'; + for (let i = 0; i < n; i++) h += `
`; + return h + '
'; + } + function cCode() { + const lines = [ + '
', + ' ', + '
', + '', + '.fade-y {', + ' --tw-fade-size: 3rem;', + ' --tw-fade-travel: 2rem; /* band open */', + ' --tw-fade-onset: 8; /* edge speed */', + '}', + '', + 'fade all four edges', + 'fade-y top + bottom', + 'fade-x start + end (RTL-aware)', + 'fade-top leading vertical only', + 'fade-bottom trailing vertical only', + ]; + let body = ''; + for (let k = 0; k < 3; k++) body += lines.join('\n') + '\n\n'; + return `
${body}
`; + } + function cDense(n) { + let h = '
'; + for (let i = 0; i < n; i++) h += `
row label ${i + 1}${(Math.sin(i) * 1000 | 0)}
`; + return h + '
'; + } + function cChips(n) { + const words = ['All', 'Design', 'Engineering', 'Marketing', 'Sales', 'Support', 'Research', 'Ops', 'Finance', 'Legal', 'People', 'Data', 'Brand', 'Growth']; + let h = '
'; + for (let i = 0; i < n; i++) h += `
${words[i % words.length]} ${i + 1}
`; + return h + '
'; + } + function cHCards(n) { + let h = '
'; + for (let i = 0; i < n; i++) h += `
Card ${i + 1}
`; + return h + '
'; + } + function cPlane(cols, rows) { + let h = `
`; + for (let i = 0; i < cols * rows; i++) h += `
${i + 1}
`; + return h + '
'; + } + + const CONTENT = { + text: () => cText(10), + cards: () => cCards(14), + images: () => cTiles(18), + code: () => cCode(), + dense: () => cDense(28), + }; + + /* ---- scenarios ----------------------------------------------------- */ + // kind: 'simple' (scroller + body html) or a named special builder. + // flex: body follows the global content-type selector when true. + const SCENARIOS = { + directions: [ + { id: 'fy', label: 'fade-y', cls: 'fade-y', body: () => cText(10), flex: true, note: 'Top + bottom. The canonical vertical scroller.' }, + { id: 'ftop', label: 'fade-top', cls: 'fade-top', body: () => cText(10), flex: true, note: 'Leading edge only; bottom is never masked.' }, + { id: 'fbot', label: 'fade-bottom', cls: 'fade-bottom', body: () => cText(10), flex: true, note: 'Trailing edge only; only dissolves near the very end.' }, + { id: 'fall', label: 'fade (all edges)', cls: 'fade', special: 'plane', note: '2-D scroll. Four mask layers intersect at the corners.' }, + { id: 'fx', label: 'fade-x', cls: 'fade-x', special: 'hscroll', content: 'chips', note: 'Inline start + end. Flips under RTL.' }, + { id: 'fstart', label: 'fade-start', cls: 'fade-start', special: 'hscroll', content: 'chips', note: 'Inline leading edge. Right edge under RTL.' }, + { id: 'fend', label: 'fade-end', cls: 'fade-end', special: 'hscroll', content: 'chips', note: 'Inline trailing edge. Left edge under RTL.' }, + ], + realworld: [ + { id: 'short', label: 'Short content', cls: 'fade-y short', special: 'short', note: 'Does NOT overflow → must show NO fade (scroll-driven path).' }, + { id: 'sticky', label: 'Sticky header', cls: 'fade-y', special: 'sticky', note: 'Sticky header sits over the faded top edge — watch for double-dim.' }, + { id: 'leadin', label: 'Padding lead-in', cls: 'fade-y', special: 'leadin', note: 'Top/bottom padding inside the scroller; fade rides the padded edge.' }, + { id: 'snap', label: 'Scroll-snap', cls: 'fade-y snap', special: 'snap', note: 'scroll-snap-type: y mandatory. Fade should track snapped offsets.' }, + { id: 'nested', label: 'Nested fade', cls: 'fade-y', special: 'nested', note: 'A fade-y scroller inside a fade-y scroller (block-axis isolation).' }, + { id: 'images', label: 'Image grid', cls: 'fade-y', special: 'simple', content: 'images', flex: true, note: 'Dense visual content; mask edge crosses tile boundaries.' }, + { id: 'code', label: 'Code block', cls: 'fade-y', special: 'simple', content: 'code', flex: true, note: 'Monospace, pre-wrapped; check baseline alignment at the edge.' }, + { id: 'tiny', label: 'Tiny container', cls: 'fade-y tiny', special: 'simple', content: 'dense', note: 'Very short viewport; size may exceed the box.' }, + { id: 'bordered', label: 'Thick border', cls: 'fade-y', special: 'bordered', note: 'Mask clips the element box — border + fade interaction.' }, + { id: 'hcards', label: 'Horizontal cards', cls: 'fade-x', special: 'hscroll', content: 'hcards', note: 'Fast flick row; both inline edges fade.' }, + { id: 'bigsize', label: 'Oversized fade (50%)', cls: 'fade-y', special: 'simple', content: 'text', lock: { vars: { '--tw-fade-size': '50%' } }, note: 'Degenerate: each band is half the viewport. Bands should not cross.' }, + { id: 'peredge', label: 'Per-edge sizes', cls: 'fade-y', special: 'simple', content: 'text', lock: { vars: { '--tw-fade-size-top': '72px', '--tw-fade-size-bottom': '16px' } }, note: 'top 72px / bottom 16px — asymmetric per-edge sizing.' }, + { id: 'clip', label: 'overflow: clip (trap)', cls: 'fade-y', special: 'clip', content: 'text', note: 'overflow:clip is not a scroll container → timeline inert → NO fade though content IS clipped.' }, + { id: 'fnone', label: 'fade-none (control)', cls: 'fade-y fade-none', special: 'simple', content: 'text', noFallback: true, note: 'Forced off — must never fade, even mid-scroll.' }, + { id: 'noney', label: 'fade-none-y on fade', cls: 'fade fade-none-y', special: 'plane', noFallback: true, note: 'All edges armed, vertical forced off → only the inline (l/r) edges fade.' }, + { id: 'alwaysx', label: 'fade-always-x on fade-y', cls: 'fade-y fade-always-x', special: 'simple', content: 'text', noFallback: true, note: 'l/r pinned to 1 but fade-y has no horizontal layer → identity mask, NO horizontal fade.' }, + ], + }; + + // One parameter swept across its scale, all cells at the global scroll offset. + const SWEEPS = [ + { + title: 'Travel sweep', key: 'travel', unit: 'px', + desc: 'Band-open speed. With the new decouple, the edge is covered immediately at every travel — only the soft band widens. Scrub to ~8px to compare.', + values: [12, 24, 48, 80, 120, 160], + }, + { + title: 'Onset sweep', key: 'onset', unit: '', + desc: 'Edge-coverage speed. onset 1 = the OLD coupled hard-clip; 8 = current default. Scrub to ~8px to see the difference.', + values: [1, 2, 4, 8, 12, 24], + }, + { + title: 'Size sweep', key: 'size', unit: 'px', + desc: 'Band width at full fade. Pure cosmetics — does not affect when the clip is covered.', + values: [24, 32, 48, 64, 96, 128], + }, + { + title: 'Clear sweep', key: 'clear', unit: 'px', + desc: 'Untouched gap before the band starts. Useful to keep a pinned first row crisp.', + values: [0, 8, 16, 24, 40], + }, + ]; + + /* ---- DOM building -------------------------------------------------- */ + const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); + + function bodyHtmlFor(scn) { + const content = (state.content !== 'auto' && scn.flex) ? state.content : scn.content; + if (content && CONTENT[content]) return CONTENT[content](); + if (content === 'chips') return cChips(26); + if (content === 'hcards') return cHCards(12); + return cText(10); + } + + function buildScroller(scn) { + const lock = scn.lock ? JSON.stringify(scn.lock) : '{}'; + const extra = scn.noFallback ? ' data-nofallback="1"' : ''; + const sp = scn.special; + let cls = 'scroller ' + scn.cls; + let styleAttr = ''; + let inner; + + if (sp === 'plane') { cls += ' h'; inner = cPlane(8, 8); } + else if (sp === 'clip') { styleAttr = ' style="overflow:clip"'; inner = bodyHtmlFor(scn); } + else if (sp === 'hscroll') { cls += ' h'; inner = bodyHtmlFor(scn); } + else if (sp === 'sticky') { + inner = '
Sticky header · stays pinned
' + cCards(14); + } + else if (sp === 'leadin') { inner = '

' + LOREM.join('

') + '

'; } + else if (sp === 'snap') { + let s = '
'; + for (let i = 0; i < 10; i++) s += `
${i + 1}
`; + inner = s + '
'; + } + else if (sp === 'short') { inner = '

' + LOREM[0] + '

'; } + else { inner = bodyHtmlFor(scn); } + + let html = `
${inner}
`; + + if (sp === 'nested') { + html = `
` + + '

Outer scroller. The inner box is its own fade-y scroll context.

' + + '
' + cDense(18) + '
' + + '

After the nested region the outer keeps scrolling so its own bottom edge fades.

' + + cText(6).replace('
', '
') + + '
'; + } + if (sp === 'bordered') { + html = `
${cCards(14)}
`; + } + return html; + } + + function buildCard(scn) { + return `
` + + `
${esc(scn.label)}` + + `${esc(scn.cls.replace('scroller', '').trim())}` + + '
' + + buildScroller(scn) + + `
${esc(scn.note || '')}
` + + '
'; + } + + function buildSweepCard(sweep, value) { + const lock = { [sweep.key]: value }; + const label = `${sweep.key} ${value}${sweep.unit}` + (sweep.key === 'onset' && value === 1 ? ' (old)' : (sweep.key === 'onset' && value === 8 ? ' (default)' : '')); + return `
${esc(label)}
` + + `
${cText(9)}
` + + '
'; + } + + function render() { + const stage = document.getElementById('stage'); + let h = ''; + + h += '

Directions

all seven public direction utilities on identical content
'; + h += '
' + SCENARIOS.directions.map(buildCard).join('') + '
'; + + h += '

Real-world & edge cases

layouts and content that have historically broken edge-fade plugins
'; + h += '
' + SCENARIOS.realworld.map(buildCard).join('') + '
'; + + SWEEPS.forEach((sw) => { + h += `

${esc(sw.title)}

${esc(sw.desc)}
`; + h += '
' + sw.values.map((v) => buildSweepCard(sw, v)).join('') + '
'; + }); + + stage.innerHTML = h; + applyStageChrome(); + applyAll(); + syncScroll(); + } + + /* ---- applying parameters ------------------------------------------ */ + function setVarPx(el, prop, value) { + if (value == null || value === '') el.style.removeProperty(prop); + else el.style.setProperty(prop, typeof value === 'number' ? value + 'px' : value); + } + + function applyTo(el) { + const lock = JSON.parse(el.dataset.lock || '{}'); + const p = { + size: 'size' in lock ? lock.size : state.size, + travel: 'travel' in lock ? lock.travel : state.travel, + onset: 'onset' in lock ? lock.onset : state.onset, + clear: 'clear' in lock ? lock.clear : state.clear, + }; + setVarPx(el, '--tw-fade-size', p.size); + setVarPx(el, '--tw-fade-travel', p.travel); + el.style.setProperty('--tw-fade-onset', String(p.onset)); + setVarPx(el, '--tw-fade-clear', p.clear); + if (lock.vars) for (const k in lock.vars) el.style.setProperty(k, lock.vars[k]); + const allowFallback = state.fallback && !el.dataset.nofallback; + el.classList.toggle('fade-always', allowFallback); + } + + function applyAll() { + document.querySelectorAll('.scroller').forEach(applyTo); + } + + function applyStageChrome() { + const stage = document.getElementById('stage'); + stage.className = 'stage bg-' + state.bg + ' shape-' + state.shape; + const grid = stage; + grid.dir = state.rtl ? 'rtl' : 'ltr'; + } + + /* ---- scroll sync --------------------------------------------------- */ + function syncScroll() { + document.querySelectorAll('.scroller').forEach((el) => { + const maxY = el.scrollHeight - el.clientHeight; + const maxX = el.scrollWidth - el.clientWidth; + const sign = state.rtl ? -1 : 1; + if (state.scrollPx != null) { + const fromEnd = state.scrollMode === 'end'; + const posY = fromEnd ? maxY - state.scrollPx : state.scrollPx; + const posX = fromEnd ? maxX - state.scrollPx : state.scrollPx; + if (maxY > 0) el.scrollTop = Math.max(0, Math.min(posY, maxY)); + if (maxX > 0) el.scrollLeft = sign * Math.max(0, Math.min(posX, maxX)); + } else { + if (maxY > 0) el.scrollTop = Math.round(state.scrollPct * maxY); + if (maxX > 0) el.scrollLeft = sign * Math.round(state.scrollPct * maxX); + } + }); + requestAnimationFrame(() => requestAnimationFrame(updateReadouts)); + } + + function updateReadouts() { + document.querySelectorAll('.card').forEach((card) => { + const out = card.querySelector('.readout'); + const el = card.querySelector('.scroller'); + if (!out || !el) return; + const maxY = el.scrollHeight - el.clientHeight; + const maxX = el.scrollWidth - el.clientWidth; + const cs = getComputedStyle(el); + const amt = (p) => (Number(cs.getPropertyValue('--tw-fade-' + p)) || 0).toFixed(2).replace(/^0(?=\.)/, ''); + if (maxX > maxY && maxX > 0) { + const x = Math.abs(el.scrollLeft); + out.textContent = `x ${x|0}/${maxX|0} · ${Math.round(x / maxX * 100)}% · l${amt('l')} r${amt('r')}`; + } else if (maxY > 0) { + out.textContent = `y ${el.scrollTop|0}/${maxY|0} · ${Math.round(el.scrollTop / maxY * 100)}% · t${amt('t')} b${amt('b')}`; + } else { + out.textContent = 'no overflow → no fade'; + } + }); + } + + /* ---- auto-scroll --------------------------------------------------- */ + function toggleAuto(on) { + if (autoTimer) { cancelAnimationFrame(autoTimer); autoTimer = null; } + if (!on) return; + const tick = () => { + state.scrollPx = null; + state.scrollPct += autoDir * 0.004; + if (state.scrollPct >= 1) { state.scrollPct = 1; autoDir = -1; } + if (state.scrollPct <= 0) { state.scrollPct = 0; autoDir = 1; } + const slider = document.getElementById('scrollRange'); + if (slider) slider.value = String(Math.round(state.scrollPct * 100)); + syncScroll(); + autoTimer = requestAnimationFrame(tick); + }; + autoTimer = requestAnimationFrame(tick); + } + + /* ---- controls ------------------------------------------------------ */ + function fmtSize(v) { return v == null ? 'default' : v + 'px'; } + + function wire() { + const $ = (id) => document.getElementById(id); + + const bind = (id, fn) => { const e = $(id); if (e) e.addEventListener('input', fn); }; + + bind('sizeRange', (e) => { + state.size = e.target.value === '0' ? null : Number(e.target.value); + $('sizeVal').textContent = fmtSize(state.size); + applyAll(); + }); + bind('travelRange', (e) => { + state.travel = e.target.value === '0' ? null : Number(e.target.value); + $('travelVal').textContent = fmtSize(state.travel); + applyAll(); syncScroll(); + }); + bind('onsetRange', (e) => { + state.onset = Number(e.target.value); + $('onsetVal').textContent = String(state.onset) + (state.onset === 1 ? ' (old)' : ''); + applyAll(); + }); + bind('clearRange', (e) => { + state.clear = Number(e.target.value); + $('clearVal').textContent = state.clear + 'px'; + applyAll(); + }); + + bind('scrollRange', (e) => { + toggleAuto(false); $('autoChk').checked = false; + state.scrollPx = null; + state.scrollPct = Number(e.target.value) / 100; + syncScroll(); + }); + + document.querySelectorAll('[data-scrollmode]').forEach((b) => b.addEventListener('click', () => { + state.scrollMode = b.dataset.scrollmode; + document.querySelectorAll('[data-scrollmode]').forEach((x) => x.setAttribute('aria-pressed', String(x === b))); + if (state.scrollPx != null) syncScroll(); + })); + + document.querySelectorAll('[data-step]').forEach((b) => b.addEventListener('click', () => { + toggleAuto(false); $('autoChk').checked = false; + const v = b.dataset.step; + if (v.endsWith('%')) { state.scrollPx = null; state.scrollPct = parseInt(v) / 100; $('scrollRange').value = parseInt(v); } + else { state.scrollPx = Number(v); } + syncScroll(); + })); + + $('autoChk').addEventListener('change', (e) => toggleAuto(e.target.checked)); + + const reRender = () => render(); + $('contentSel').addEventListener('change', (e) => { state.content = e.target.value; reRender(); }); + $('bgSel').addEventListener('change', (e) => { state.bg = e.target.value; applyStageChrome(); }); + $('shapeSel').addEventListener('change', (e) => { state.shape = e.target.value; applyStageChrome(); }); + + $('rtlChk').addEventListener('change', (e) => { state.rtl = e.target.checked; applyStageChrome(); syncScroll(); }); + $('fallbackChk').addEventListener('change', (e) => { + state.fallback = e.target.checked; + e.target.closest('.toggle').dataset.on = String(state.fallback); + applyAll(); + }); + $('resetBtn').addEventListener('click', () => { + Object.assign(state, { size: null, travel: null, onset: 8, clear: 0, scrollPct: 0, scrollPx: null, fallback: false }); + toggleAuto(false); + document.querySelectorAll('input,select').forEach((el) => { + if (el.type === 'checkbox') el.checked = false; + }); + $('sizeRange').value = 0; $('sizeVal').textContent = 'default'; + $('travelRange').value = 0; $('travelVal').textContent = 'default'; + $('onsetRange').value = 8; $('onsetVal').textContent = '8'; + $('clearRange').value = 0; $('clearVal').textContent = '0px'; + $('scrollRange').value = 0; + render(); + }); + + // Re-sync on real user scroll of any single card (so readouts stay honest) + document.getElementById('stage').addEventListener('scroll', (e) => { + if (e.target.classList && e.target.classList.contains('scroller')) { + requestAnimationFrame(updateReadouts); + } + }, true); + + window.addEventListener('resize', () => syncScroll()); + } + + /* ---- init ---------------------------------------------------------- */ + function init() { + render(); + wire(); + } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); + else init(); +})(); diff --git a/src/tw-fade.css b/src/tw-fade.css index 1c04e54..6c77def 100644 --- a/src/tw-fade.css +++ b/src/tw-fade.css @@ -1,4 +1,4 @@ -/* tw-fade — CSS scroll-edge fade masking for Tailwind v4. MIT · github.com/petekp/tw-fade */ +/* tw-fade - CSS scroll-edge fade masking for Tailwind v4. MIT - github.com/petekp/tw-fade */ /* Amounts: keep so scroll animation interpolates them; initial 0 = no fade. */ @property --tw-fade-t { @@ -21,7 +21,12 @@ inherits: false; initial-value: 0; } -/* Config: keep "*" with NO initial-value — a typed rejects the rem initial, dropping the registration (fade collapses); inherits:false stops size and mask-layer declarations leaking into nested fades. */ + +/* +Config: keep "*" with NO initial-value. A typed rejects the rem +fallback and drops the registration in browsers; inherits:false stops nested +fades from leaking size, travel, clearance, mask, or routing state. +*/ @property --tw-fade-size { syntax: "*"; inherits: false; @@ -34,39 +39,51 @@ syntax: "*"; inherits: false; } -@property --tw-fade-size-t { +@property --tw-fade-size-top { + syntax: "*"; + inherits: false; +} +@property --tw-fade-size-bottom { syntax: "*"; inherits: false; } -@property --tw-fade-size-b { +@property --tw-fade-size-start { syntax: "*"; inherits: false; } -@property --tw-fade-size-l { +@property --tw-fade-size-end { syntax: "*"; inherits: false; } -@property --tw-fade-size-r { +@property --tw-fade-travel { syntax: "*"; inherits: false; } -@property --tw-fade-range { +@property --tw-fade-clear { syntax: "*"; inherits: false; } -@property --tw-fade-clear-t { +@property --tw-fade-clear-y { syntax: "*"; inherits: false; } -@property --tw-fade-clear-b { +@property --tw-fade-clear-x { syntax: "*"; inherits: false; } -@property --tw-fade-clear-l { +@property --tw-fade-clear-top { syntax: "*"; inherits: false; } -@property --tw-fade-clear-r { +@property --tw-fade-clear-bottom { + syntax: "*"; + inherits: false; +} +@property --tw-fade-clear-start { + syntax: "*"; + inherits: false; +} +@property --tw-fade-clear-end { syntax: "*"; inherits: false; } @@ -86,6 +103,70 @@ syntax: "*"; inherits: false; } +@property --tw-fade-ltr-start-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-layer { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-animation { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-timeline { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-start-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-ltr-end-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-start-animation-range { + syntax: "*"; + inherits: false; +} +@property --tw-fade-rtl-end-animation-range { + syntax: "*"; + inherits: false; +} @theme { --fade-size-xs: calc(var(--spacing, 0.25rem) * 6); @@ -96,14 +177,14 @@ --fade-size-2xl: calc(var(--spacing, 0.25rem) * 24); --fade-size-3xl: calc(var(--spacing, 0.25rem) * 32); --fade-size-4xl: calc(var(--spacing, 0.25rem) * 40); - --fade-range-xs: calc(var(--spacing, 0.25rem) * 6); - --fade-range-sm: calc(var(--spacing, 0.25rem) * 8); - --fade-range-md: calc(var(--spacing, 0.25rem) * 12); - --fade-range-lg: calc(var(--spacing, 0.25rem) * 16); - --fade-range-xl: calc(var(--spacing, 0.25rem) * 20); - --fade-range-2xl: calc(var(--spacing, 0.25rem) * 24); - --fade-range-3xl: calc(var(--spacing, 0.25rem) * 32); - --fade-range-4xl: calc(var(--spacing, 0.25rem) * 40); + --fade-travel-xs: calc(var(--spacing, 0.25rem) * 6); + --fade-travel-sm: calc(var(--spacing, 0.25rem) * 8); + --fade-travel-md: calc(var(--spacing, 0.25rem) * 12); + --fade-travel-lg: calc(var(--spacing, 0.25rem) * 16); + --fade-travel-xl: calc(var(--spacing, 0.25rem) * 20); + --fade-travel-2xl: calc(var(--spacing, 0.25rem) * 24); + --fade-travel-3xl: calc(var(--spacing, 0.25rem) * 32); + --fade-travel-4xl: calc(var(--spacing, 0.25rem) * 40); --fade-clear-xs: calc(var(--spacing, 0.25rem) * 6); --fade-clear-sm: calc(var(--spacing, 0.25rem) * 8); --fade-clear-md: calc(var(--spacing, 0.25rem) * 12); @@ -114,7 +195,7 @@ --fade-clear-4xl: calc(var(--spacing, 0.25rem) * 40); } -@keyframes tw-fade-reveal-t { +@keyframes tw-fade-travel-t { from { --tw-fade-t: 0; } @@ -122,7 +203,7 @@ --tw-fade-t: 1; } } -@keyframes tw-fade-reveal-b { +@keyframes tw-fade-travel-b { from { --tw-fade-b: 1; } @@ -130,7 +211,7 @@ --tw-fade-b: 0; } } -@keyframes tw-fade-reveal-l { +@keyframes tw-fade-travel-l { from { --tw-fade-l: 0; } @@ -138,7 +219,7 @@ --tw-fade-l: 1; } } -@keyframes tw-fade-reveal-r { +@keyframes tw-fade-travel-r { from { --tw-fade-r: 1; } @@ -146,304 +227,417 @@ --tw-fade-r: 0; } } +@keyframes tw-fade-travel-r-leading { + from { + --tw-fade-r: 0; + } + to { + --tw-fade-r: 1; + } +} +@keyframes tw-fade-travel-l-trailing { + from { + --tw-fade-l: 1; + } + to { + --tw-fade-l: 0; + } +} @utility fade-size-* { --tw-fade-size: --value(--fade-size-*, [length], [percentage]); } -@utility fade-size-t-* { - --tw-fade-size-t: --value(--fade-size-*, [length], [percentage]); -} -@utility fade-size-b-* { - --tw-fade-size-b: --value(--fade-size-*, [length], [percentage]); -} -@utility fade-size-l-* { - --tw-fade-size-l: --value(--fade-size-*, [length], [percentage]); -} -@utility fade-size-r-* { - --tw-fade-size-r: --value(--fade-size-*, [length], [percentage]); -} @utility fade-size-y-* { --tw-fade-size-y: --value(--fade-size-*, [length], [percentage]); } @utility fade-size-x-* { --tw-fade-size-x: --value(--fade-size-*, [length], [percentage]); } -@utility fade-range-* { - --tw-fade-range: --value(--fade-range-*, [length], [percentage]); +@utility fade-size-top-* { + --tw-fade-size-top: --value(--fade-size-*, [length], [percentage]); +} +@utility fade-size-bottom-* { + --tw-fade-size-bottom: --value(--fade-size-*, [length], [percentage]); } -@utility fade-clear-t-* { - --tw-fade-clear-t: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-t: --value(--fade-clear-*, [length], [percentage]); +@utility fade-size-start-* { + --tw-fade-size-start: --value(--fade-size-*, [length], [percentage]); } -@utility fade-clear-b-* { - --tw-fade-clear-b: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-b: --value(--fade-clear-*, [length], [percentage]); +@utility fade-size-end-* { + --tw-fade-size-end: --value(--fade-size-*, [length], [percentage]); } -@utility fade-clear-l-* { - --tw-fade-clear-l: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-l: --value(--fade-clear-*, [length], [percentage]); +@utility fade-travel-* { + --tw-fade-travel: --value(--fade-travel-*, [length], [percentage]); } -@utility fade-clear-r-* { - --tw-fade-clear-r: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-r: --value(--fade-clear-*, [length], [percentage]); +@utility fade-clear-* { + --tw-fade-clear: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear: --value(--fade-clear-*, [length], [percentage]); } @utility fade-clear-y-* { - --tw-fade-clear-t: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-t: --value(--fade-clear-*, [length], [percentage]); - --tw-fade-clear-b: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-b: --value(--fade-clear-*, [length], [percentage]); + --tw-fade-clear-y: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear-y: --value(--fade-clear-*, [length], [percentage]); } @utility fade-clear-x-* { - --tw-fade-clear-l: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-l: --value(--fade-clear-*, [length], [percentage]); - --tw-fade-clear-r: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-r: --value(--fade-clear-*, [length], [percentage]); + --tw-fade-clear-x: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear-x: --value(--fade-clear-*, [length], [percentage]); } -@utility fade-clear-xy-* { - --tw-fade-clear-t: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-t: --value(--fade-clear-*, [length], [percentage]); - --tw-fade-clear-b: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-b: --value(--fade-clear-*, [length], [percentage]); - --tw-fade-clear-l: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-l: --value(--fade-clear-*, [length], [percentage]); - --tw-fade-clear-r: calc(var(--spacing, 0.25rem) * --value(integer)); - --tw-fade-clear-r: --value(--fade-clear-*, [length], [percentage]); +@utility fade-clear-top-* { + --tw-fade-clear-top: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear-top: --value(--fade-clear-*, [length], [percentage]); } -@utility fade-clear-t-var { - --tw-fade-clear-t: var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px))); +@utility fade-clear-bottom-* { + --tw-fade-clear-bottom: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear-bottom: --value(--fade-clear-*, [length], [percentage]); } -@utility fade-clear-b-var { - --tw-fade-clear-b: var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px))); +@utility fade-clear-start-* { + --tw-fade-clear-start: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear-start: --value(--fade-clear-*, [length], [percentage]); } -@utility fade-clear-l-var { - --tw-fade-clear-l: var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px))); +@utility fade-clear-end-* { + --tw-fade-clear-end: calc(var(--spacing, 0.25rem) * --value(integer)); + --tw-fade-clear-end: --value(--fade-clear-*, [length], [percentage]); } -@utility fade-clear-r-var { - --tw-fade-clear-r: var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px))); +@utility fade-clear-var { + --tw-fade-clear: var(--fade-clear, 0px); } @utility fade-clear-y-var { - --tw-fade-clear-t: var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px))); - --tw-fade-clear-b: var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px))); + --tw-fade-clear-y: var(--fade-clear-y, var(--fade-clear, 0px)); } @utility fade-clear-x-var { - --tw-fade-clear-l: var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px))); - --tw-fade-clear-r: var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px))); + --tw-fade-clear-x: var(--fade-clear-x, var(--fade-clear, 0px)); +} +@utility fade-clear-top-var { + --tw-fade-clear-top: var(--fade-clear-top, var(--fade-clear-y, var(--fade-clear, 0px))); +} +@utility fade-clear-bottom-var { + --tw-fade-clear-bottom: var(--fade-clear-bottom, var(--fade-clear-y, var(--fade-clear, 0px))); } -@utility fade-clear-xy-var { - --tw-fade-clear-t: var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px))); - --tw-fade-clear-b: var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px))); - --tw-fade-clear-l: var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px))); - --tw-fade-clear-r: var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px))); +@utility fade-clear-start-var { + --tw-fade-clear-start: var(--fade-clear-start, var(--fade-clear-x, var(--fade-clear, 0px))); +} +@utility fade-clear-end-var { + --tw-fade-clear-end: var(--fade-clear-end, var(--fade-clear-x, var(--fade-clear, 0px))); } -/* Keep shared defaults on public fade elements, not :root: root aliases would - resolve before local theme overrides, and range aliases would miss - fade-range utilities. */ @layer utilities { - .fade-t, - .fade-b, - .fade-l, - .fade-r, + .fade, .fade-y, + .fade-top, + .fade-bottom, .fade-x, - .fade-xy { - --tw-fade-size-default: var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)); - --tw-fade-range-active: var(--tw-fade-range, var(--fade-range-md, calc(var(--spacing, 0.25rem) * 12))); + .fade-start, + .fade-end { + --tw-fade-size-default: min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12))); + --tw-fade-travel-active: var(--tw-fade-travel, var(--fade-travel-sm, calc(var(--spacing, 0.25rem) * 8))); + --tw-fade-identity-mask: linear-gradient(#000, #000); + --tw-fade-edge-size-t: var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-b: var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-l: var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-r: var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-clear-t: var(--tw-fade-clear-top, var(--tw-fade-clear-y, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-b: var(--tw-fade-clear-bottom, var(--tw-fade-clear-y, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-l: var(--tw-fade-clear-start, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-r: var(--tw-fade-clear-end, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); + --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); + --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); + --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); + /* + Decouple edge transparency from band growth. --tw-fade-t..r drive the + band WIDTH (above), so the band eases open over the full travel distance. + But the ALPHA at the very edge is driven by --tw-fade-alpha-*, a + saturated copy that reaches full transparency within travel/onset of + scroll. This covers the overflow clip almost immediately (no hard cut + during early scroll) while the band still widens cosmetically over the + whole travel. --tw-fade-onset = how fast the edge goes transparent + relative to the travel; higher = snappier clip coverage. At t=0 alpha is + also 0, so a flush (unscrolled) edge still shows no fade. + */ + --tw-fade-onset: 8; + --tw-fade-alpha-t: min(1, calc(var(--tw-fade-t) * var(--tw-fade-onset))); + --tw-fade-alpha-b: min(1, calc(var(--tw-fade-b) * var(--tw-fade-onset))); + --tw-fade-alpha-l: min(1, calc(var(--tw-fade-l) * var(--tw-fade-onset))); + --tw-fade-alpha-r: min(1, calc(var(--tw-fade-r) * var(--tw-fade-onset))); + --tw-fade-gradient-t: linear-gradient( + to bottom, + #000 0 var(--tw-fade-edge-clear-t), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t))) var(--tw-fade-edge-clear-t), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.983)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.933)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.854)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.75)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.629)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.5)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.371)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.25)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.146)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.067)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-t) * 0.017)) + calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) + ); + --tw-fade-gradient-b: linear-gradient( + to top, + #000 0 var(--tw-fade-edge-clear-b), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b))) var(--tw-fade-edge-clear-b), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.983)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.933)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.854)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.75)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.629)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.5)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.371)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.25)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.146)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.067)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-b) * 0.017)) + calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) + ); + --tw-fade-gradient-l: linear-gradient( + to right, + #000 0 var(--tw-fade-edge-clear-l), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l))) var(--tw-fade-edge-clear-l), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.983)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.933)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.854)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.75)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.629)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.5)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.371)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.25)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.146)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.067)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-l) * 0.017)) + calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) + ); + --tw-fade-gradient-r: linear-gradient( + to left, + #000 0 var(--tw-fade-edge-clear-r), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r))) var(--tw-fade-edge-clear-r), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.983)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.933)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.854)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.75)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.629)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.5)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.371)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.25)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.146)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.067)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), + rgb(0 0 0 / calc(1 - var(--tw-fade-alpha-r) * 0.017)) + calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), + rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) + ); + --tw-fade-mask-l: var(--tw-fade-ltr-start-layer, var(--tw-fade-identity-mask)); + --tw-fade-mask-r: var(--tw-fade-ltr-end-layer, var(--tw-fade-identity-mask)); + --tw-fade-animation-l: var(--tw-fade-ltr-start-animation, none); + --tw-fade-animation-r: var(--tw-fade-ltr-end-animation, none); + --tw-fade-timeline-l: var(--tw-fade-ltr-start-timeline, none); + --tw-fade-timeline-r: var(--tw-fade-ltr-end-timeline, none); + --tw-fade-animation-range-l: var(--tw-fade-ltr-start-animation-range, normal); + --tw-fade-animation-range-r: var(--tw-fade-ltr-end-animation-range, normal); + /* + Neutral block-axis defaults. Unlike the l/r hooks above, the t/b + animation/timeline/range vars are not @property-registered and are set + only in the axis blocks below, so without a local default here a nested + fade child would inherit a parent's vertical hook and run a spurious + (inert) second scroll animation. The axis blocks override these by + source order at equal specificity, so top-level fades are unaffected. + */ + --tw-fade-animation-t: none; + --tw-fade-animation-b: none; + --tw-fade-timeline-t: none; + --tw-fade-timeline-b: none; + --tw-fade-animation-range-t: normal; + --tw-fade-animation-range-b: normal; + mask-image: + var(--tw-fade-mask-t, var(--tw-fade-identity-mask)), + var(--tw-fade-mask-b, var(--tw-fade-identity-mask)), + var(--tw-fade-mask-l, var(--tw-fade-identity-mask)), + var(--tw-fade-mask-r, var(--tw-fade-identity-mask)); + mask-composite: intersect; + mask-repeat: no-repeat; + mask-size: 100% 100%; + } + + .fade:where(:dir(rtl)), + .fade-y:where(:dir(rtl)), + .fade-top:where(:dir(rtl)), + .fade-bottom:where(:dir(rtl)), + .fade-x:where(:dir(rtl)), + .fade-start:where(:dir(rtl)), + .fade-end:where(:dir(rtl)) { + --tw-fade-edge-size-l: var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-size-r: var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); + --tw-fade-edge-clear-l: var(--tw-fade-clear-end, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-edge-clear-r: var(--tw-fade-clear-start, var(--tw-fade-clear-x, var(--tw-fade-clear, 0px))); + --tw-fade-mask-l: var(--tw-fade-rtl-end-layer, var(--tw-fade-identity-mask)); + --tw-fade-mask-r: var(--tw-fade-rtl-start-layer, var(--tw-fade-identity-mask)); + --tw-fade-animation-l: var(--tw-fade-rtl-end-animation, none); + --tw-fade-animation-r: var(--tw-fade-rtl-start-animation, none); + --tw-fade-timeline-l: var(--tw-fade-rtl-end-timeline, none); + --tw-fade-timeline-r: var(--tw-fade-rtl-start-timeline, none); + --tw-fade-animation-range-l: var(--tw-fade-rtl-end-animation-range, normal); + --tw-fade-animation-range-r: var(--tw-fade-rtl-start-animation-range, normal); + } + + .fade, + .fade-y, + .fade-top { + --tw-fade-mask-t: var(--tw-fade-gradient-t); + --tw-fade-animation-t: tw-fade-travel-t; + --tw-fade-timeline-t: scroll(self y); + --tw-fade-animation-range-t: 0 var(--tw-fade-travel-active); + } + + .fade, + .fade-y, + .fade-bottom { + --tw-fade-mask-b: var(--tw-fade-gradient-b); + --tw-fade-animation-b: tw-fade-travel-b; + --tw-fade-timeline-b: scroll(self y); + --tw-fade-animation-range-b: calc(100% - var(--tw-fade-travel-active)) 100%; + } + + .fade, + .fade-x, + .fade-start { + --tw-fade-ltr-start-layer: var(--tw-fade-gradient-l); + --tw-fade-rtl-start-layer: var(--tw-fade-gradient-r); + --tw-fade-ltr-start-animation: tw-fade-travel-l; + --tw-fade-rtl-start-animation: tw-fade-travel-r-leading; + --tw-fade-ltr-start-timeline: scroll(self inline); + --tw-fade-rtl-start-timeline: scroll(self inline); + --tw-fade-ltr-start-animation-range: 0 var(--tw-fade-travel-active); + --tw-fade-rtl-start-animation-range: 0 var(--tw-fade-travel-active); + } + + .fade, + .fade-x, + .fade-end { + --tw-fade-ltr-end-layer: var(--tw-fade-gradient-r); + --tw-fade-rtl-end-layer: var(--tw-fade-gradient-l); + --tw-fade-ltr-end-animation: tw-fade-travel-r; + --tw-fade-rtl-end-animation: tw-fade-travel-l-trailing; + --tw-fade-ltr-end-timeline: scroll(self inline); + --tw-fade-rtl-end-timeline: scroll(self inline); + --tw-fade-ltr-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%; + --tw-fade-rtl-end-animation-range: calc(100% - var(--tw-fade-travel-active)) 100%; } -} -/* Private (only @apply'd by fade-*). The gradient must live on the element so var(--tw-fade-t) resolves per-element — a :root copy would bake in --tw-fade-t:0. */ -@utility tw-fade-mask { - mask-image: - var(--tw-fade-mask-t, linear-gradient(#000, #000)), - var(--tw-fade-mask-b, linear-gradient(#000, #000)), - var(--tw-fade-mask-l, linear-gradient(#000, #000)), - var(--tw-fade-mask-r, linear-gradient(#000, #000)); - mask-composite: intersect; - mask-repeat: no-repeat; - mask-size: 100% 100%; @supports (animation-timeline: scroll()) { - animation: - tw-fade-reveal-t auto linear both, - tw-fade-reveal-b auto linear both, - tw-fade-reveal-l auto linear both, - tw-fade-reveal-r auto linear both; - animation-timeline: - scroll(self y), scroll(self y), scroll(self x), scroll(self x); - animation-range: - 0 var(--tw-fade-range-active), - calc(100% - var(--tw-fade-range-active)) 100%, - 0 var(--tw-fade-range-active), - calc(100% - var(--tw-fade-range-active)) 100%; + .fade, + .fade-y, + .fade-top, + .fade-bottom, + .fade-x, + .fade-start, + .fade-end { + animation: + var(--tw-fade-animation-t, none) auto linear both, + var(--tw-fade-animation-b, none) auto linear both, + var(--tw-fade-animation-l, none) auto linear both, + var(--tw-fade-animation-r, none) auto linear both; + animation-timeline: + var(--tw-fade-timeline-t, none), + var(--tw-fade-timeline-b, none), + var(--tw-fade-timeline-l, none), + var(--tw-fade-timeline-r, none); + animation-range: + var(--tw-fade-animation-range-t, normal), + var(--tw-fade-animation-range-b, normal), + var(--tw-fade-animation-range-l, normal), + var(--tw-fade-animation-range-r, normal); + } } + @supports not (animation-timeline: scroll()) { - --tw-fade-t: 1; - --tw-fade-b: 1; - --tw-fade-l: 1; - --tw-fade-r: 1; + .fade, + .fade-y, + .fade-top, + .fade-bottom, + .fade-x, + .fade-start, + .fade-end { + --tw-fade-t: 1; + --tw-fade-b: 1; + --tw-fade-l: 1; + --tw-fade-r: 1; + } } -} -@utility tw-fade-edge-t { - --tw-fade-edge-size-t: var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-t: var(--tw-fade-clear-t, 0px); - --tw-fade-edge-band-t: calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t)); - --tw-fade-mask-t: linear-gradient( - to bottom, - #000 0 var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t))) var(--tw-fade-edge-clear-t), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.983)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.933)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.854)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.75)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.629)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.5)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.371)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.25)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.146)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.067)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-t) * 0.017)) - calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-t) + var(--tw-fade-edge-band-t)) - ); -} -@utility tw-fade-edge-b { - --tw-fade-edge-size-b: var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-b: var(--tw-fade-clear-b, 0px); - --tw-fade-edge-band-b: calc(var(--tw-fade-edge-size-b) * var(--tw-fade-b)); - --tw-fade-mask-b: linear-gradient( - to top, - #000 0 var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b))) var(--tw-fade-edge-clear-b), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.983)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.933)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.854)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.75)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.629)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.5)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.371)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.25)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.146)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.067)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-b) * 0.017)) - calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-b) + var(--tw-fade-edge-band-b)) - ); -} -@utility tw-fade-edge-l { - --tw-fade-edge-size-l: var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-l: var(--tw-fade-clear-l, 0px); - --tw-fade-edge-band-l: calc(var(--tw-fade-edge-size-l) * var(--tw-fade-l)); - --tw-fade-mask-l: linear-gradient( - to right, - #000 0 var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l))) var(--tw-fade-edge-clear-l), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.983)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.933)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.854)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.75)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.629)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.5)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.371)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.25)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.146)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.067)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-l) * 0.017)) - calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-l) + var(--tw-fade-edge-band-l)) - ); -} -@utility tw-fade-edge-r { - --tw-fade-edge-size-r: var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default)))); - --tw-fade-edge-clear-r: var(--tw-fade-clear-r, 0px); - --tw-fade-edge-band-r: calc(var(--tw-fade-edge-size-r) * var(--tw-fade-r)); - --tw-fade-mask-r: linear-gradient( - to left, - #000 0 var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r))) var(--tw-fade-edge-clear-r), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.983)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.08333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.933)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.16667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.854)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.25), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.75)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.33333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.629)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.41667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.5)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.5), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.371)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.58333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.25)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.66667), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.146)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.75), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.067)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.83333), - rgb(0 0 0 / calc(1 - var(--tw-fade-r) * 0.017)) - calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r) * 0.91667), - rgb(0 0 0 / 1) calc(var(--tw-fade-edge-clear-r) + var(--tw-fade-edge-band-r)) - ); -} -/* Public utilities select mask layers; tw-fade-mask owns the shared scroll animation so edge utilities compose. */ -@utility fade-t { - @apply tw-fade-mask tw-fade-edge-t; -} -@utility fade-b { - @apply tw-fade-mask tw-fade-edge-b; -} -@utility fade-l { - @apply tw-fade-mask tw-fade-edge-l; -} -@utility fade-r { - @apply tw-fade-mask tw-fade-edge-r; -} -@utility fade-y { - @apply tw-fade-mask tw-fade-edge-t tw-fade-edge-b; -} -@utility fade-x { - @apply tw-fade-mask tw-fade-edge-l tw-fade-edge-r; -} -@utility fade-xy { - @apply tw-fade-mask tw-fade-edge-t tw-fade-edge-b tw-fade-edge-l tw-fade-edge-r; -} + .fade-none { + --tw-fade-t: 0 !important; + --tw-fade-b: 0 !important; + --tw-fade-l: 0 !important; + --tw-fade-r: 0 !important; + } + + .fade-none-y { + --tw-fade-t: 0 !important; + --tw-fade-b: 0 !important; + } -/* !important so the static fade outranks the running animation (which itself outranks normal author decls); order-independent vs the edge utility. */ -@utility fade-static { - --tw-fade-t: 1 !important; - --tw-fade-b: 1 !important; - --tw-fade-l: 1 !important; - --tw-fade-r: 1 !important; - animation-name: none; + .fade-none-x { + --tw-fade-l: 0 !important; + --tw-fade-r: 0 !important; + } + + .fade-always { + --tw-fade-t: 1 !important; + --tw-fade-b: 1 !important; + --tw-fade-l: 1 !important; + --tw-fade-r: 1 !important; + } + + .fade-always-y { + --tw-fade-t: 1 !important; + --tw-fade-b: 1 !important; + } + + .fade-always-x { + --tw-fade-l: 1 !important; + --tw-fade-r: 1 !important; + } } diff --git a/test/plugin.test.mjs b/test/plugin.test.mjs index 9aa3b19..e98459c 100644 --- a/test/plugin.test.mjs +++ b/test/plugin.test.mjs @@ -1,36 +1,26 @@ /** * Unit tests for tw-fade. * - * npm test (runs `node --test`) - * - * tw-fade is now a pure Tailwind v4 CSS plugin: src/tw-fade.css is the single - * source of truth, authored with @utility / @theme / --value(). These tests - * compile that source the same way the shipped dist is built (scripts/build-css - * → the v4 CLI, utilities-only, isolated) and assert on the REAL emitted CSS. - * That keeps them honest: they exercise the exact bytes a consumer's gets, - * not a hand-rolled model of what the plugin "should" produce. - * - * They lock in the structural invariants that make the effect correct: the - * registered custom properties (and the @property computational-independence - * rule that the "only large fades" bug taught us), the scroll-driven reveal - * wiring, the multi-layer intersect-composited mask, per-edge isolation, and the - * 13-stop smoothstep ramp. End-to-end visual behaviour is covered separately by - * build/verify*.mjs (which drive real Chromium). + * These tests compile src/tw-fade.css through the same Tailwind v4 path used by + * scripts/build-css.mjs, then assert on the emitted CSS bytes. Browser-level + * behavior is covered by build/verify*.mjs. */ import { test } from 'node:test' import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' import { compileCss } from '../scripts/build-css.mjs' +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, '..') + const css = compileCss() -const integerClearCss = compileCss({ - classes: 'fade-clear-t-14 fade-clear-b-14 fade-clear-l-14 fade-clear-r-14 fade-clear-y-14 fade-clear-x-14 fade-clear-xy-14', +const customCss = compileCss({ + classes: + 'fade-size-[15%] fade-size-top-[56px] fade-travel-[80px] fade-clear-14 fade-clear-y-14 fade-clear-x-14 fade-clear-top-14 fade-clear-bottom-14 fade-clear-start-14 fade-clear-end-14', }) -/** - * Return the balanced-brace body of the first ` {` block — including any - * nested at-rules (@supports …). A tiny brace scanner beats a regex here because - * the utility blocks legitimately nest braces. - */ function block(selector, source = css) { const at = source.indexOf(selector + ' {') if (at === -1) return null @@ -43,7 +33,6 @@ function block(selector, source = css) { return null } -/** Parse an @property registration into { syntax, inherits, initial-value }. */ function property(name) { const body = block('@property ' + name) if (body == null) return null @@ -54,18 +43,19 @@ function property(name) { return { syntax: field('syntax'), inherits: field('inherits'), 'initial-value': field('initial-value') } } -/** The value of a single declaration `prop: …;` within a block body. */ function declValue(body, prop) { - const m = body.match(new RegExp(`${prop}:\\s*([^;]+);`)) + const m = body?.match(new RegExp(`${prop}:\\s*([^;]+);`)) return m ? m[1].trim() : undefined } -const EDGES = { '.fade-t': 't', '.fade-b': 'b', '.fade-l': 'l', '.fade-r': 'r' } -const ALL = ['.fade-t', '.fade-b', '.fade-l', '.fade-r', '.fade-y', '.fade-x', '.fade-xy'] -const MASK_VARS = ['--tw-fade-mask-t', '--tw-fade-mask-b', '--tw-fade-mask-l', '--tw-fade-mask-r'] -const SIZE_VARS = ['--tw-fade-size', '--tw-fade-size-y', '--tw-fade-size-x', '--tw-fade-size-t', '--tw-fade-size-b', '--tw-fade-size-l', '--tw-fade-size-r'] -const CLEAR_VARS = ['--tw-fade-clear-t', '--tw-fade-clear-b', '--tw-fade-clear-l', '--tw-fade-clear-r'] -const SHARED_DEFAULTS_SELECTOR = '.fade-t, .fade-b, .fade-l, .fade-r, .fade-y, .fade-x, .fade-xy' +function classSelectors(source = css) { + return new Set( + [...source.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)] + .map((m) => m[1]) + .filter((name) => name === 'fade' || name.startsWith('fade-')), + ) +} + const SCALE = [ ['xs', 6], ['sm', 8], @@ -76,15 +66,76 @@ const SCALE = [ ['3xl', 32], ['4xl', 40], ] -const DEFAULT_SIZE_DECL = 'var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12))' -const DEFAULT_RANGE_DECL = 'var(--fade-range-md, calc(var(--spacing, 0.25rem) * 12))' -const RANGE_ACTIVE_DECL = `var(--tw-fade-range, ${DEFAULT_RANGE_DECL})` -const EDGE_SIZE_DECLS = { - t: 'var(--tw-fade-size-t, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))))', - b: 'var(--tw-fade-size-b, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))))', - l: 'var(--tw-fade-size-l, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default))))', - r: 'var(--tw-fade-size-r, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default))))', -} + +const PUBLIC_DIRECTIONS = ['fade', 'fade-y', 'fade-top', 'fade-bottom', 'fade-x', 'fade-start', 'fade-end'] +// Classes that must NOT compile, both groups intentional: +// - the old physical API removed in 0.7.0 (fade-t/b/l/r, fade-xy, fade-static, fade-left/right); +// - the fully-logical block/inline aliases from an earlier draft. The shipped API deliberately +// uses plain directions (physical top/bottom, direction-aware start/end) instead of +// fade-inline-start / fade-block-* — see "Why Plain Directions" in MIGRATING.md. Re-adding +// any of these should be a conscious decision, not an accidental regression. +const REJECTED_CLASSES = [ + 'fade-t', + 'fade-b', + 'fade-l', + 'fade-r', + 'fade-xy', + 'fade-left', + 'fade-right', + 'fade-block', + 'fade-block-start', + 'fade-block-end', + 'fade-inline', + 'fade-inline-start', + 'fade-inline-end', + 'fade-range-sm', + 'fade-ramp-sm', + 'fade-static', +] +const SHARED_SELECTOR = '.fade, .fade-y, .fade-top, .fade-bottom, .fade-x, .fade-start, .fade-end' +const RTL_SELECTOR = + '.fade:where(:dir(rtl)), .fade-y:where(:dir(rtl)), .fade-top:where(:dir(rtl)), .fade-bottom:where(:dir(rtl)), .fade-x:where(:dir(rtl)), .fade-start:where(:dir(rtl)), .fade-end:where(:dir(rtl))' +const TOP_SELECTOR = '.fade, .fade-y, .fade-top' +const BOTTOM_SELECTOR = '.fade, .fade-y, .fade-bottom' +const START_SELECTOR = '.fade, .fade-x, .fade-start' +const END_SELECTOR = '.fade, .fade-x, .fade-end' +const MASK_VARS = ['--tw-fade-mask-t', '--tw-fade-mask-b', '--tw-fade-mask-l', '--tw-fade-mask-r'] +const SIZE_VARS = [ + '--tw-fade-size', + '--tw-fade-size-y', + '--tw-fade-size-x', + '--tw-fade-size-top', + '--tw-fade-size-bottom', + '--tw-fade-size-start', + '--tw-fade-size-end', +] +const CLEAR_VARS = [ + '--tw-fade-clear', + '--tw-fade-clear-y', + '--tw-fade-clear-x', + '--tw-fade-clear-top', + '--tw-fade-clear-bottom', + '--tw-fade-clear-start', + '--tw-fade-clear-end', +] +const ROUTING_VARS = [ + '--tw-fade-ltr-start-layer', + '--tw-fade-ltr-end-layer', + '--tw-fade-rtl-start-layer', + '--tw-fade-rtl-end-layer', + '--tw-fade-ltr-start-animation', + '--tw-fade-ltr-end-animation', + '--tw-fade-rtl-start-animation', + '--tw-fade-rtl-end-animation', + '--tw-fade-ltr-start-timeline', + '--tw-fade-ltr-end-timeline', + '--tw-fade-rtl-start-timeline', + '--tw-fade-rtl-end-timeline', + '--tw-fade-ltr-start-animation-range', + '--tw-fade-ltr-end-animation-range', + '--tw-fade-rtl-start-animation-range', + '--tw-fade-rtl-end-animation-range', +] const ALPHA_MULTIPLIERS = [ '0.983', '0.933', @@ -112,26 +163,16 @@ const POSITION_MULTIPLIERS = [ '0.91667', ] -test('registers the amounts as typed and config/mask layers as universal, non-inheriting', () => { +test('registers amounts as typed numbers and config/routing vars as universal non-inheriting', () => { for (const amt of ['--tw-fade-t', '--tw-fade-b', '--tw-fade-l', '--tw-fade-r']) { const prop = property(amt) assert.ok(prop, `missing @property ${amt}`) - // A typed (not `*`) is what lets scroll-driven animation interpolate - // the amount SMOOTHLY instead of snapping between keyframes. assert.equal(prop.syntax, '""') assert.equal(prop.inherits, 'false') - // Initial 0 = no fade: the correct base state when the timeline is inactive. assert.equal(prop['initial-value'], '0') } - // --tw-fade-size* / --tw-fade-range / --tw-fade-clear-* / --tw-fade-mask-* ARE registered, but with the UNIVERSAL - // syntax and NO initial-value. A typed @property would reject the - // font-relative (rem) default and silently drop the registration — the "only - // large fades" bug. The universal syntax sidesteps that rule; the omitted - // initial-value keeps an unset value guaranteed-invalid, so `var(--tw-fade-size, - // )` still falls back to the default. The point of registering at all is - // `inherits: false`, which stops a nested fade from leaking its parent's - // size/range/clearance or mask layers. - for (const v of [...SIZE_VARS, '--tw-fade-range', ...CLEAR_VARS, ...MASK_VARS]) { + + for (const v of [...SIZE_VARS, '--tw-fade-travel', ...CLEAR_VARS, ...MASK_VARS, ...ROUTING_VARS]) { const prop = property(v) assert.ok(prop, `missing @property ${v}`) assert.equal(prop.syntax, '"*"') @@ -141,16 +182,9 @@ test('registers the amounts as typed and config/mask layers as universa }) test('every typed @property has a computationally-independent initial-value', () => { - // Regression guard for the "only the large panel fades" bug: a registered - // @property with a TYPED syntax (e.g. ) whose initial-value uses a - // context-relative unit (rem/em/%/vh/cqw/lh/…) is REJECTED by the browser, - // leaving the variable unset. We assert the REAL invariant — typed syntax pairs - // with an independent initial — via an ABSOLUTE-unit allowlist (case- - // insensitive, future-proof against new relative units). The universal syntax - // (`*`) carries no such rule and legitimately omits initial-value, so it's exempt. const ABSOLUTE = /^-?(?:\d+\.?\d*|\.\d+)(px|cm|mm|in|pt|pc|q)?$/i const names = [...css.matchAll(/@property\s+(--[\w-]+)\s*\{/g)].map((m) => m[1]) - assert.ok(names.length >= 10, 'expected at least 10 registered properties') + assert.ok(names.length >= 20, 'expected registered properties') for (const name of names) { const decl = property(name) if (decl.syntax === '"*"') { @@ -166,300 +200,304 @@ test('every typed @property has a computationally-independent initial-value', () } }) -test('leading edges reveal 0→1, trailing edges hide 1→0', () => { - const kf = (n) => block('@keyframes ' + n) - assert.match(kf('tw-fade-reveal-t'), /from\s*\{[^}]*--tw-fade-t:\s*0/) - assert.match(kf('tw-fade-reveal-t'), /to\s*\{[^}]*--tw-fade-t:\s*1/) - assert.match(kf('tw-fade-reveal-b'), /from\s*\{[^}]*--tw-fade-b:\s*1/) - assert.match(kf('tw-fade-reveal-b'), /to\s*\{[^}]*--tw-fade-b:\s*0/) - assert.match(kf('tw-fade-reveal-l'), /from\s*\{[^}]*--tw-fade-l:\s*0/) - assert.match(kf('tw-fade-reveal-r'), /from\s*\{[^}]*--tw-fade-r:\s*1/) +test('direction inventory matches the plain public API and stays within budget', () => { + const selectors = classSelectors() + for (const name of PUBLIC_DIRECTIONS) assert.ok(selectors.has(name), `missing .${name}`) + for (const name of REJECTED_CLASSES) assert.equal(selectors.has(name), false, `must not emit .${name}`) + assert.ok(selectors.size <= 150, `selector budget exceeded: ${selectors.size}`) + assert.ok(css.length <= 55_000, `byte budget exceeded: ${css.length}`) }) -test('every edge utility carries the composited multi-layer mask base', () => { - for (const cls of ALL) { - const u = block(cls) - assert.ok(u, `missing ${cls}`) - // intersect is mandatory: the initial `add` (union) would erase the fade. - assert.equal(declValue(u, 'mask-composite'), 'intersect') - assert.equal(declValue(u, 'mask-repeat'), 'no-repeat') - assert.equal(declValue(u, 'mask-size'), '100% 100%') - // Four layers, each defaulting to the opaque identity (no-op) gradient. - const layers = u.split('linear-gradient(#000, #000)').length - 1 - assert.equal(layers, 4, `${cls} should have 4 identity fallbacks`) - } +test('prebuilt dist is generated from the current default compile output', () => { + const dist = fs.readFileSync(path.join(root, 'dist', 'tw-fade.css'), 'utf8') + assert.equal(dist, css) }) -test('each directional utility sets ONLY its own mask layer', () => { - for (const [cls, edge] of Object.entries(EDGES)) { - const u = block(cls) - const mine = `--tw-fade-mask-${edge}` - for (const v of MASK_VARS) { - // A real declaration is `--tw-fade-mask-x: …`; a var() reference is `--tw-fade-mask-x, …`. - // Matching the colon distinguishes the two, so the shared 4-layer fallback - // (which references all four) doesn't read as "this utility sets all four". - if (v === mine) assert.ok(declValue(u, v), `${cls} must set ${v}`) - else assert.equal(declValue(u, v), undefined, `${cls} must NOT set ${v}`) +// Guards against a dormant retired utility name surviving in the shipped CSS. +// The REJECTED_CLASSES checks only prove the *default* compile omits a name; a +// stray @utility / @property / @keyframes / theme token for a retired name +// ("range" -> "ramp" -> "travel", and the never-shipped intermediate "reveal") +// could still ship undetected. Scan the source and prebuilt artifacts on disk. +// Scoped to the CSS files only — README/MIGRATING/CHANGELOG legitimately +// reference the old names when documenting the rename. Token-specific patterns, +// so the English nouns "reveal"/"ramp" in prose/comments are never matched. +test('shipped CSS carries no retired reveal/range/ramp token names', () => { + const retired = [ + /fade-reveal/, + /fade-range/, + /fade-ramp/, + /--fade-reveal/, + /--fade-range/, + /--fade-ramp/, + /--tw-fade-reveal/, + /--tw-fade-range/, + /--tw-fade-ramp/, + /tw-fade-reveal/, + /tw-fade-range/, + /tw-fade-ramp/, + ] + for (const file of ['src/tw-fade.css', 'dist/tw-fade.css']) { + const source = fs.readFileSync(path.join(root, file), 'utf8') + for (const pattern of retired) { + assert.equal(pattern.test(source), false, `${file} still contains retired token ${pattern}`) } } }) -test('axis shorthands set exactly their constituent mask layers', () => { - const y = block('.fade-y') - assert.ok(declValue(y, '--tw-fade-mask-t') && declValue(y, '--tw-fade-mask-b')) - assert.equal(declValue(y, '--tw-fade-mask-l'), undefined) - const x = block('.fade-x') - assert.ok(declValue(x, '--tw-fade-mask-l') && declValue(x, '--tw-fade-mask-r')) - assert.equal(declValue(x, '--tw-fade-mask-t'), undefined) - const xy = block('.fade-xy') - for (const v of MASK_VARS) assert.ok(declValue(xy, v), `xy must set ${v}`) +test('leading and trailing keyframes include RTL horizontal variants', () => { + const kf = (n) => block('@keyframes ' + n) + assert.match(kf('tw-fade-travel-t'), /from\s*\{[^}]*--tw-fade-t:\s*0/) + assert.match(kf('tw-fade-travel-t'), /to\s*\{[^}]*--tw-fade-t:\s*1/) + assert.match(kf('tw-fade-travel-b'), /from\s*\{[^}]*--tw-fade-b:\s*1/) + assert.match(kf('tw-fade-travel-b'), /to\s*\{[^}]*--tw-fade-b:\s*0/) + assert.match(kf('tw-fade-travel-l'), /from\s*\{[^}]*--tw-fade-l:\s*0/) + assert.match(kf('tw-fade-travel-r'), /from\s*\{[^}]*--tw-fade-r:\s*1/) + assert.match(kf('tw-fade-travel-r-leading'), /from\s*\{[^}]*--tw-fade-r:\s*0/) + assert.match(kf('tw-fade-travel-r-leading'), /to\s*\{[^}]*--tw-fade-r:\s*1/) + assert.match(kf('tw-fade-travel-l-trailing'), /from\s*\{[^}]*--tw-fade-l:\s*1/) + assert.match(kf('tw-fade-travel-l-trailing'), /to\s*\{[^}]*--tw-fade-l:\s*0/) +}) + +test('the shared mask setup owns four physical layers and scroll animation wiring', () => { + const shared = block(SHARED_SELECTOR) + assert.ok(shared, 'missing shared direction block') + assert.equal(declValue(shared, 'mask-composite'), 'intersect') + assert.equal(declValue(shared, 'mask-repeat'), 'no-repeat') + assert.equal(declValue(shared, 'mask-size'), '100% 100%') + const maskImage = declValue(shared, 'mask-image') + for (const v of MASK_VARS) assert.match(maskImage, new RegExp(`var\\(${v.replaceAll('-', '\\-')}`), `${v} in mask-image`) + + const supports = block('@supports (animation-timeline: scroll())') + const animated = block(SHARED_SELECTOR, supports) + assert.ok(animated, 'missing animation support block') + assert.equal((declValue(animated, 'animation').match(/auto linear both/g) || []).length, 4) + assert.equal((declValue(animated, 'animation-timeline').match(/var\(--tw-fade-timeline-/g) || []).length, 4) + assert.equal((declValue(animated, 'animation-range').match(/var\(--tw-fade-animation-range-/g) || []).length, 4) }) -test('mask gradients point the right way and are driven by their amount', () => { - assert.match(declValue(block('.fade-t'), '--tw-fade-mask-t'), /^linear-gradient\(\s*to bottom/) - assert.match(declValue(block('.fade-b'), '--tw-fade-mask-b'), /^linear-gradient\(\s*to top/) - assert.match(declValue(block('.fade-l'), '--tw-fade-mask-l'), /^linear-gradient\(\s*to right/) - assert.match(declValue(block('.fade-r'), '--tw-fade-mask-r'), /^linear-gradient\(\s*to left/) - // The amount scales both the gradient alpha (1 - amount * …) and its length - // (size * amount), so at amount 0 the layer collapses to fully opaque. - const tBlock = block('.fade-t') - const t = declValue(tBlock, '--tw-fade-mask-t') - assert.match(t, /calc\(1 - var\(--tw-fade-t\)\)/) - // Size is resolved through a shared default alias, then multiplied by the - // scroll-gated amount. This keeps the gradient readable without weakening the - // "no size utility still fades" invariant. - assert.equal(declValue(block(SHARED_DEFAULTS_SELECTOR), '--tw-fade-size-default'), DEFAULT_SIZE_DECL) - assert.equal(declValue(tBlock, '--tw-fade-edge-size-t'), EDGE_SIZE_DECLS.t) - assert.equal(declValue(tBlock, '--tw-fade-edge-band-t'), 'calc(var(--tw-fade-edge-size-t) * var(--tw-fade-t))') - // Clearance is an opaque edge band before the fade ramp. This lets sticky - // headers/footers stay unmasked while rows beneath still dissolve at the edge. - assert.equal(declValue(tBlock, '--tw-fade-edge-clear-t'), 'var(--tw-fade-clear-t, 0px)') - assert.match(t, /#000 0 var\(--tw-fade-edge-clear-t\)/) - assert.match(t, /rgb\(0 0 0 \/ calc\(1 - var\(--tw-fade-t\)\)\) var\(--tw-fade-edge-clear-t\)/) +test('direction classes select the expected layers and central RTL routing', () => { + const top = block(TOP_SELECTOR) + assert.equal(declValue(top, '--tw-fade-mask-t'), 'var(--tw-fade-gradient-t)') + assert.equal(declValue(top, '--tw-fade-animation-t'), 'tw-fade-travel-t') + assert.equal(declValue(top, '--tw-fade-timeline-t'), 'scroll(self y)') + + const bottom = block(BOTTOM_SELECTOR) + assert.equal(declValue(bottom, '--tw-fade-mask-b'), 'var(--tw-fade-gradient-b)') + assert.equal(declValue(bottom, '--tw-fade-animation-b'), 'tw-fade-travel-b') + assert.equal(declValue(bottom, '--tw-fade-timeline-b'), 'scroll(self y)') + + const start = block(START_SELECTOR) + assert.equal(declValue(start, '--tw-fade-ltr-start-layer'), 'var(--tw-fade-gradient-l)') + assert.equal(declValue(start, '--tw-fade-rtl-start-layer'), 'var(--tw-fade-gradient-r)') + assert.equal(declValue(start, '--tw-fade-ltr-start-animation'), 'tw-fade-travel-l') + assert.equal(declValue(start, '--tw-fade-rtl-start-animation'), 'tw-fade-travel-r-leading') + assert.equal(declValue(start, '--tw-fade-ltr-start-timeline'), 'scroll(self inline)') + assert.equal(declValue(start, '--tw-fade-rtl-start-timeline'), 'scroll(self inline)') + + const end = block(END_SELECTOR) + assert.equal(declValue(end, '--tw-fade-ltr-end-layer'), 'var(--tw-fade-gradient-r)') + assert.equal(declValue(end, '--tw-fade-rtl-end-layer'), 'var(--tw-fade-gradient-l)') + assert.equal(declValue(end, '--tw-fade-ltr-end-animation'), 'tw-fade-travel-r') + assert.equal(declValue(end, '--tw-fade-rtl-end-animation'), 'tw-fade-travel-l-trailing') + + const rtl = block(RTL_SELECTOR) + assert.ok(rtl, 'missing central RTL routing block') + assert.equal(declValue(rtl, '--tw-fade-mask-l'), 'var(--tw-fade-rtl-end-layer, var(--tw-fade-identity-mask))') + assert.equal(declValue(rtl, '--tw-fade-mask-r'), 'var(--tw-fade-rtl-start-layer, var(--tw-fade-identity-mask))') + assert.equal(declValue(rtl, '--tw-fade-animation-l'), 'var(--tw-fade-rtl-end-animation, none)') + assert.equal(declValue(rtl, '--tw-fade-animation-r'), 'var(--tw-fade-rtl-start-animation, none)') + assert.equal(declValue(rtl, '--tw-fade-edge-size-l'), 'var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default))))') + assert.equal(declValue(rtl, '--tw-fade-edge-size-r'), 'var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default))))') }) test('the smoothstep ramp is the 13-stop sigmoid from the reference', () => { - for (const [cls, edge] of Object.entries(EDGES)) { - const grad = declValue(block(cls), `--tw-fade-mask-${edge}`) + const shared = block(SHARED_SELECTOR) + const edges = { t: '--tw-fade-gradient-t', b: '--tw-fade-gradient-b', l: '--tw-fade-gradient-l', r: '--tw-fade-gradient-r' } + for (const [edge, prop] of Object.entries(edges)) { + const grad = declValue(shared, prop) + assert.ok(grad, `missing ${prop}`) const stops = (grad.match(/rgb\(0 0 0 \//g) || []).length - assert.equal(stops, 13, `${cls} gradient should have 13 colour stops`) + assert.equal(stops, 13, `${prop} should have 13 colour stops`) const alphaMultipliers = [ - ...grad.matchAll(new RegExp(`var\\(--tw-fade-${edge}\\)\\s*\\*\\s*([0-9.]+)`, 'g')), + ...grad.matchAll(new RegExp(`var\\(--tw-fade-alpha-${edge}\\)\\s*\\*\\s*([0-9.]+)`, 'g')), ].map((m) => m[1]) - assert.deepEqual(alphaMultipliers, ALPHA_MULTIPLIERS, `${cls} alpha ramp coefficients must stay exact`) + assert.deepEqual(alphaMultipliers, ALPHA_MULTIPLIERS, `${prop} alpha ramp coefficients must stay exact`) const positionMultipliers = [ ...grad.matchAll(new RegExp(`var\\(--tw-fade-edge-band-${edge}\\)\\s*\\*\\s*([0-9.]+)`, 'g')), ].map((m) => m[1]) - assert.deepEqual(positionMultipliers, POSITION_MULTIPLIERS, `${cls} position ramp coefficients must stay exact`) + assert.deepEqual(positionMultipliers, POSITION_MULTIPLIERS, `${prop} position ramp coefficients must stay exact`) } - // The curve endpoints: first stop fully transparent-capable (1 - amount), last - // stop fully opaque (rgb(0 0 0 / 1)) at the band's far edge, offset after any - // sticky-item clearance band. - const t = declValue(block('.fade-t'), '--tw-fade-mask-t') - assert.match(t, /rgb\(0 0 0 \/ calc\(1 - var\(--tw-fade-t\)\)\) var\(--tw-fade-edge-clear-t\),/) - assert.match( - t, - new RegExp(String.raw`rgb\(0 0 0 \/ 1\) calc\(var\(--tw-fade-edge-clear-t\) \+ var\(--tw-fade-edge-band-t\)\)\s*\)\s*$`), - ) + assert.match(declValue(shared, '--tw-fade-gradient-t'), /^linear-gradient\(\s*to bottom/) + assert.match(declValue(shared, '--tw-fade-gradient-b'), /^linear-gradient\(\s*to top/) + assert.match(declValue(shared, '--tw-fade-gradient-l'), /^linear-gradient\(\s*to right/) + assert.match(declValue(shared, '--tw-fade-gradient-r'), /^linear-gradient\(\s*to left/) }) -test('every public fade utility uses the shared four-edge reveal setup', () => { - // Public utilities need the same animation shorthand/longhands so arbitrary - // combinations like `fade-t fade-r` compose instead of last-declaration-wins. - // Physical axes (scroll(self y) / scroll(self x)) match the mask gradients. - const defaults = block(SHARED_DEFAULTS_SELECTOR) - assert.equal(declValue(defaults, '--tw-fade-range-active'), RANGE_ACTIVE_DECL) - assert.ok(!css.includes('--tw-fade-range-default'), 'range default should be folded into --tw-fade-range-active') - for (const cls of ALL) { - const u = block(cls) - const s = block('@supports (animation-timeline: scroll())', block(cls)) - assert.ok(s, `${cls} must include the scroll-timeline support block`) - assert.equal(declValue(s, 'animation').split(',').length, 4, `${cls} must animate all four amounts`) - assert.match(s, /animation-timeline:\s*scroll\(self y\), scroll\(self y\), scroll\(self x\), scroll\(self x\)/) - // Leading reveals over the first range; trailing hides over the last. Range - // is resolved once by the shared defaults rule (same fix as --tw-fade-size). - assert.equal(declValue(u, '--tw-fade-range-active'), undefined) +test('edge alpha is decoupled from band growth so the clip is covered before the band finishes widening', () => { + const shared = block(SHARED_SELECTOR) + assert.ok(shared, 'missing shared block') + + // The onset constant sets how fast the edge reaches full transparency relative + // to the travel: full coverage by travel/onset of scroll, independent of band width. + assert.equal(declValue(shared, '--tw-fade-onset'), '8') + + for (const edge of ['t', 'b', 'l', 'r']) { + // alpha-* is a saturated copy of raw scroll progress (clamped at 1). This is + // what drives edge transparency, so the clip is covered almost immediately. + assert.equal( + declValue(shared, `--tw-fade-alpha-${edge}`), + `min(1, calc(var(--tw-fade-${edge}) * var(--tw-fade-onset)))`, + `--tw-fade-alpha-${edge} must saturate raw progress`, + ) + // Band WIDTH stays keyed to raw progress, so it still eases open over the + // full travel distance (cosmetic widening) while the edge is already covered. + assert.equal( + declValue(shared, `--tw-fade-edge-band-${edge}`), + `calc(var(--tw-fade-edge-size-${edge}) * var(--tw-fade-${edge}))`, + `band size for ${edge} must stay on raw --tw-fade-${edge}`, + ) + const grad = declValue(shared, `--tw-fade-gradient-${edge}`) + // Every alpha stop reads the saturated alpha var... assert.match( - s, - new RegExp( - String.raw`animation-range:\s*0 var\(--tw-fade-range-active\), calc\(100% - var\(--tw-fade-range-active\)\) 100%, 0 var\(--tw-fade-range-active\), calc\(100% - var\(--tw-fade-range-active\)\) 100%`, - ), + grad, + new RegExp(`calc\\(1 - var\\(--tw-fade-alpha-${edge}\\)`), + `${edge} gradient alpha must read the saturated alpha var`, + ) + // ...and never raw progress, or the clip would re-couple to the full travel. + assert.doesNotMatch( + grad, + new RegExp(`calc\\(1 - var\\(--tw-fade-${edge}\\)`), + `${edge} gradient alpha must not key off raw --tw-fade-${edge}`, ) } }) -test('provides a static fallback when scroll-driven animation is unsupported', () => { - for (const cls of ALL) { - const fb = block('@supports not (animation-timeline: scroll())', block(cls)) - assert.ok(fb, `${cls} must include the static fallback block`) - for (const amt of ['--tw-fade-t', '--tw-fade-b', '--tw-fade-l', '--tw-fade-r']) { - assert.equal(declValue(fb, amt), '1', `${cls} fallback must pin ${amt}`) - } - } -}) - -test('fade-static forces the fade on, order-independently, and disables the animation', () => { - const s = block('.fade-static') - // The amounts are pinned with !important so fade-static wins regardless of where - // Tailwind sorts it relative to the reveal animation — an important author value - // outranks a running keyframe animation in the cascade. +test('static fallback pins selected fades on when scroll-driven animation is unsupported', () => { + const fallback = block('@supports not (animation-timeline: scroll())') + const shared = block(SHARED_SELECTOR, fallback) + assert.ok(shared, 'missing fallback block') for (const amt of ['--tw-fade-t', '--tw-fade-b', '--tw-fade-l', '--tw-fade-r']) { - assert.equal(declValue(s, amt), '1 !important', `${amt} must be pinned !important`) + assert.equal(declValue(shared, amt), '1') } - assert.equal(declValue(s, 'animation-name'), 'none') }) -test('exposes the size and range scales as theme-backed utilities', () => { +test('exposes the size and travel scales as theme-backed utilities', () => { for (const [name] of SCALE) { assert.equal(declValue(block(`.fade-size-${name}`), '--tw-fade-size'), `var(--fade-size-${name})`) - assert.equal(declValue(block(`.fade-size-t-${name}`), '--tw-fade-size-t'), `var(--fade-size-${name})`) - assert.equal(declValue(block(`.fade-size-b-${name}`), '--tw-fade-size-b'), `var(--fade-size-${name})`) - assert.equal(declValue(block(`.fade-size-l-${name}`), '--tw-fade-size-l'), `var(--fade-size-${name})`) - assert.equal(declValue(block(`.fade-size-r-${name}`), '--tw-fade-size-r'), `var(--fade-size-${name})`) assert.equal(declValue(block(`.fade-size-y-${name}`), '--tw-fade-size-y'), `var(--fade-size-${name})`) assert.equal(declValue(block(`.fade-size-x-${name}`), '--tw-fade-size-x'), `var(--fade-size-${name})`) - assert.equal(declValue(block(`.fade-range-${name}`), '--tw-fade-range'), `var(--fade-range-${name})`) + assert.equal(declValue(block(`.fade-size-top-${name}`), '--tw-fade-size-top'), `var(--fade-size-${name})`) + assert.equal(declValue(block(`.fade-size-bottom-${name}`), '--tw-fade-size-bottom'), `var(--fade-size-${name})`) + assert.equal(declValue(block(`.fade-size-start-${name}`), '--tw-fade-size-start'), `var(--fade-size-${name})`) + assert.equal(declValue(block(`.fade-size-end-${name}`), '--tw-fade-size-end'), `var(--fade-size-${name})`) + assert.equal(declValue(block(`.fade-travel-${name}`), '--tw-fade-travel'), `var(--fade-travel-${name})`) } - // The theme exposes the actual scale values for consumers to override/extend. - const root = block(':root, :host') + const rootBlock = block(':root, :host') for (const [name, units] of SCALE) { const expected = `calc(var(--spacing, 0.25rem) * ${units})` - assert.equal(declValue(root, `--fade-size-${name}`), expected) - assert.equal(declValue(root, `--fade-range-${name}`), expected) - } - // No bare `.fade-size` / `.fade-range` no-op utility leaks out. - assert.equal(block('.fade-size'), null) - assert.equal(block('.fade-range'), null) -}) - -test('each edge resolves size as edge over axis over global over default', () => { - const defaults = block(SHARED_DEFAULTS_SELECTOR) - assert.equal(declValue(defaults, '--tw-fade-size-default'), DEFAULT_SIZE_DECL) - for (const [cls, edge] of Object.entries(EDGES)) { - const u = block(cls) - assert.equal(declValue(u, '--tw-fade-size-default'), undefined) - assert.equal(declValue(u, `--tw-fade-edge-size-${edge}`), EDGE_SIZE_DECLS[edge]) - assert.equal(declValue(u, `--tw-fade-edge-band-${edge}`), `calc(var(--tw-fade-edge-size-${edge}) * var(--tw-fade-${edge}))`) + assert.equal(declValue(rootBlock, `--fade-size-${name}`), expected) + assert.equal(declValue(rootBlock, `--fade-travel-${name}`), expected) } + assert.equal(block('.fade-range-sm'), null) + assert.equal(block('.fade-size-left-sm'), null) }) -test('exposes edge-clearance utilities for sticky content', () => { - for (const [name] of SCALE) { - assert.equal(declValue(block(`.fade-clear-t-${name}`), '--tw-fade-clear-t'), `var(--fade-clear-${name})`) - assert.equal(declValue(block(`.fade-clear-b-${name}`), '--tw-fade-clear-b'), `var(--fade-clear-${name})`) - assert.equal(declValue(block(`.fade-clear-l-${name}`), '--tw-fade-clear-l'), `var(--fade-clear-${name})`) - assert.equal(declValue(block(`.fade-clear-r-${name}`), '--tw-fade-clear-r'), `var(--fade-clear-${name})`) - - const y = block(`.fade-clear-y-${name}`) - assert.equal(declValue(y, '--tw-fade-clear-t'), `var(--fade-clear-${name})`) - assert.equal(declValue(y, '--tw-fade-clear-b'), `var(--fade-clear-${name})`) - assert.equal(declValue(y, '--tw-fade-clear-l'), undefined) - - const x = block(`.fade-clear-x-${name}`) - assert.equal(declValue(x, '--tw-fade-clear-l'), `var(--fade-clear-${name})`) - assert.equal(declValue(x, '--tw-fade-clear-r'), `var(--fade-clear-${name})`) - assert.equal(declValue(x, '--tw-fade-clear-t'), undefined) - - const xy = block(`.fade-clear-xy-${name}`) - for (const v of CLEAR_VARS) assert.equal(declValue(xy, v), `var(--fade-clear-${name})`, `xy must set ${v}`) - } - - const root = block(':root, :host') - for (const [name, units] of SCALE) { - assert.equal(declValue(root, `--fade-clear-${name}`), `calc(var(--spacing, 0.25rem) * ${units})`) - } - assert.equal(block('.fade-clear'), null) -}) - -test('supports integer spacing values for edge clearance in the Tailwind source path', () => { - const expected = 'calc(var(--spacing, 0.25rem) * 14)' - assert.equal(declValue(block('.fade-clear-t-14', integerClearCss), '--tw-fade-clear-t'), expected) - assert.equal(declValue(block('.fade-clear-b-14', integerClearCss), '--tw-fade-clear-b'), expected) - assert.equal(declValue(block('.fade-clear-l-14', integerClearCss), '--tw-fade-clear-l'), expected) - assert.equal(declValue(block('.fade-clear-r-14', integerClearCss), '--tw-fade-clear-r'), expected) - - const y = block('.fade-clear-y-14', integerClearCss) - assert.equal(declValue(y, '--tw-fade-clear-t'), expected) - assert.equal(declValue(y, '--tw-fade-clear-b'), expected) - assert.equal(declValue(y, '--tw-fade-clear-l'), undefined) - - const x = block('.fade-clear-x-14', integerClearCss) - assert.equal(declValue(x, '--tw-fade-clear-l'), expected) - assert.equal(declValue(x, '--tw-fade-clear-r'), expected) - assert.equal(declValue(x, '--tw-fade-clear-t'), undefined) - - const xy = block('.fade-clear-xy-14', integerClearCss) - for (const v of CLEAR_VARS) assert.equal(declValue(xy, v), expected, `integer xy must set ${v}`) -}) - -test('exposes public variable utilities for dynamic edge clearance', () => { +test('size resolves as edge over axis over global over capped default', () => { + const shared = block(SHARED_SELECTOR) + assert.equal(declValue(shared, '--tw-fade-size-default'), 'min(12%, var(--fade-size-md, calc(var(--spacing, 0.25rem) * 12)))') assert.equal( - declValue(block('.fade-clear-t-var'), '--tw-fade-clear-t'), - 'var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px)))', + declValue(shared, '--tw-fade-edge-size-t'), + 'var(--tw-fade-size-top, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))))', ) assert.equal( - declValue(block('.fade-clear-b-var'), '--tw-fade-clear-b'), - 'var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px)))', + declValue(shared, '--tw-fade-edge-size-b'), + 'var(--tw-fade-size-bottom, var(--tw-fade-size-y, var(--tw-fade-size, var(--tw-fade-size-default))))', ) assert.equal( - declValue(block('.fade-clear-l-var'), '--tw-fade-clear-l'), - 'var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px)))', + declValue(shared, '--tw-fade-edge-size-l'), + 'var(--tw-fade-size-start, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default))))', ) assert.equal( - declValue(block('.fade-clear-r-var'), '--tw-fade-clear-r'), - 'var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px)))', + declValue(shared, '--tw-fade-edge-size-r'), + 'var(--tw-fade-size-end, var(--tw-fade-size-x, var(--tw-fade-size, var(--tw-fade-size-default))))', ) +}) - const y = block('.fade-clear-y-var') - assert.equal( - declValue(y, '--tw-fade-clear-t'), - 'var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px)))', - ) - assert.equal( - declValue(y, '--tw-fade-clear-b'), - 'var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px)))', - ) - assert.equal(declValue(y, '--tw-fade-clear-l'), undefined) +test('arbitrary size and travel utilities compile only through the source path', () => { + assert.equal(declValue(block('.fade-size-\\[15\\%\\]', customCss), '--tw-fade-size'), '15%') + assert.equal(declValue(block('.fade-size-top-\\[56px\\]', customCss), '--tw-fade-size-top'), '56px') + assert.equal(declValue(block('.fade-travel-\\[80px\\]', customCss), '--tw-fade-travel'), '80px') + assert.equal(block('.fade-size-\\[15\\%\\]'), null) + assert.equal(block('.fade-travel-\\[80px\\]'), null) +}) - const x = block('.fade-clear-x-var') - assert.equal( - declValue(x, '--tw-fade-clear-l'), - 'var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px)))', - ) - assert.equal( - declValue(x, '--tw-fade-clear-r'), - 'var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px)))', - ) - assert.equal(declValue(x, '--tw-fade-clear-t'), undefined) +test('exposes directional clear utilities and source-path integer clear values', () => { + for (const [name] of SCALE) { + assert.equal(declValue(block(`.fade-clear-${name}`), '--tw-fade-clear'), `var(--fade-clear-${name})`) + assert.equal(declValue(block(`.fade-clear-y-${name}`), '--tw-fade-clear-y'), `var(--fade-clear-${name})`) + assert.equal(declValue(block(`.fade-clear-x-${name}`), '--tw-fade-clear-x'), `var(--fade-clear-${name})`) + assert.equal(declValue(block(`.fade-clear-top-${name}`), '--tw-fade-clear-top'), `var(--fade-clear-${name})`) + assert.equal(declValue(block(`.fade-clear-bottom-${name}`), '--tw-fade-clear-bottom'), `var(--fade-clear-${name})`) + assert.equal(declValue(block(`.fade-clear-start-${name}`), '--tw-fade-clear-start'), `var(--fade-clear-${name})`) + assert.equal(declValue(block(`.fade-clear-end-${name}`), '--tw-fade-clear-end'), `var(--fade-clear-${name})`) + } + const expected = 'calc(var(--spacing, 0.25rem) * 14)' + assert.equal(declValue(block('.fade-clear-14', customCss), '--tw-fade-clear'), expected) + assert.equal(declValue(block('.fade-clear-top-14', customCss), '--tw-fade-clear-top'), expected) + assert.equal(declValue(block('.fade-clear-start-14', customCss), '--tw-fade-clear-start'), expected) + assert.equal(block('.fade-clear-left-sm'), null) + assert.equal(block('.fade-clear-xy-sm'), null) +}) - const xy = block('.fade-clear-xy-var') +test('dynamic clear variables follow edge to axis to global precedence', () => { + assert.equal(declValue(block('.fade-clear-var'), '--tw-fade-clear'), 'var(--fade-clear, 0px)') + assert.equal(declValue(block('.fade-clear-y-var'), '--tw-fade-clear-y'), 'var(--fade-clear-y, var(--fade-clear, 0px))') + assert.equal(declValue(block('.fade-clear-x-var'), '--tw-fade-clear-x'), 'var(--fade-clear-x, var(--fade-clear, 0px))') assert.equal( - declValue(xy, '--tw-fade-clear-t'), - 'var(--fade-clear-t, var(--fade-clear-y, var(--fade-clear-xy, 0px)))', + declValue(block('.fade-clear-top-var'), '--tw-fade-clear-top'), + 'var(--fade-clear-top, var(--fade-clear-y, var(--fade-clear, 0px)))', ) assert.equal( - declValue(xy, '--tw-fade-clear-b'), - 'var(--fade-clear-b, var(--fade-clear-y, var(--fade-clear-xy, 0px)))', + declValue(block('.fade-clear-bottom-var'), '--tw-fade-clear-bottom'), + 'var(--fade-clear-bottom, var(--fade-clear-y, var(--fade-clear, 0px)))', ) assert.equal( - declValue(xy, '--tw-fade-clear-l'), - 'var(--fade-clear-l, var(--fade-clear-x, var(--fade-clear-xy, 0px)))', + declValue(block('.fade-clear-start-var'), '--tw-fade-clear-start'), + 'var(--fade-clear-start, var(--fade-clear-x, var(--fade-clear, 0px)))', ) assert.equal( - declValue(xy, '--tw-fade-clear-r'), - 'var(--fade-clear-r, var(--fade-clear-x, var(--fade-clear-xy, 0px)))', + declValue(block('.fade-clear-end-var'), '--tw-fade-clear-end'), + 'var(--fade-clear-end, var(--fade-clear-x, var(--fade-clear, 0px)))', ) }) +test('fade-none and fade-always pin amounts without disabling shared animations', () => { + const none = block('.fade-none') + for (const amt of ['--tw-fade-t', '--tw-fade-b', '--tw-fade-l', '--tw-fade-r']) { + assert.equal(declValue(none, amt), '0 !important') + } + const noneY = block('.fade-none-y') + assert.equal(declValue(noneY, '--tw-fade-t'), '0 !important') + assert.equal(declValue(noneY, '--tw-fade-b'), '0 !important') + assert.equal(declValue(noneY, '--tw-fade-l'), undefined) + + const noneX = block('.fade-none-x') + assert.equal(declValue(noneX, '--tw-fade-l'), '0 !important') + assert.equal(declValue(noneX, '--tw-fade-r'), '0 !important') + assert.equal(declValue(noneX, '--tw-fade-t'), undefined) + + const always = block('.fade-always') + for (const amt of ['--tw-fade-t', '--tw-fade-b', '--tw-fade-l', '--tw-fade-r']) { + assert.equal(declValue(always, amt), '1 !important') + } + assert.ok(!/animation-name:\s*none/.test(css), 'always utilities must not disable shared animations') + assert.ok(css.indexOf('.fade-none') < css.indexOf('.fade-always'), 'fade-always should be the emitted source-order winner') +}) + test('the framework-free build is real CSS, self-contained, with no Tailwind noise', () => { - // Source-only constructs must be fully compiled away. for (const token of ['@utility', '@theme', '@apply', '--value(']) { assert.ok(!css.includes(token), `compiled output must not contain ${token}`) } - // No Preflight / theme-dump leakage from the utilities-only, isolated build. - // Our own internal namespace is --tw-fade-*; any OTHER --tw-* would be Tailwind - // core noise, so exclude our prefix from the leak check. assert.ok(!/--tw-(?!fade-)/.test(css), 'no Tailwind core --tw-* variables should leak in') assert.ok(!/\.flex\s*\{|\.grid\s*\{|\.border\s*\{/.test(css), 'no built-in utilities should leak in') - assert.equal(block('.tw-fade-mask'), null, 'private tw-fade-mask utility should not be emitted as a public class') })