@cssints/gradient 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexei Accio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,94 @@
1
- # Temporary Holding Version
1
+ # @cssints/gradient
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Eased gradients for [cssints](../../README.md): a gradient from one colour to another along an easing curve, expanded into
4
+ colour stops at build time, so the fade has no hard edge where a two-stop gradient shows one. It is one value member of
5
+ `cssints/plugin` `scope()` and returns an `<image>`, so it goes where an image goes (`backgroundImage`, `maskImage`,
6
+ `borderImageSource`). No runtime.
7
+
8
+ ```ts
9
+ import { backgroundImage, createTokens } from "cssints" with { type: "cssints" };
10
+ import { gradient } from "@cssints/gradient" with { type: "cssints" };
11
+
12
+ const t = createTokens({ color: { $type: "color", ink: { $value: "#111111" } } });
13
+
14
+ // gradient(kind, direction, from, to, easing?, stops?)
15
+ export const fade = backgroundImage(
16
+ gradient("linear", "to bottom", "#000", "transparent", "ease-in-out", 5),
17
+ );
18
+ export const glow = backgroundImage(
19
+ gradient("radial", "circle at top", "red", "blue", "cubic-bezier(0.5, 0, 0.5, 1)", 4),
20
+ );
21
+ export const ink = backgroundImage(
22
+ gradient("linear", "to right", t.color.ink, "white", "ease-in", 4),
23
+ );
24
+ ```
25
+
26
+ ```css
27
+ ._4we9ve {
28
+ background-image: linear-gradient(
29
+ to bottom,
30
+ #000,
31
+ #000000d7 27.44%,
32
+ #00000080 50%,
33
+ #00000028 72.56%,
34
+ transparent
35
+ );
36
+ }
37
+ ._ba8wye {
38
+ background-image: radial-gradient(circle at top, red, #c44a6f 37.04%, #5348d0 62.96%, blue);
39
+ }
40
+ ._1l9gl59 {
41
+ background-image: linear-gradient(
42
+ to right,
43
+ var(--color-ink),
44
+ color-mix(in oklab, var(--color-ink), white 25.93%) 44.59%,
45
+ color-mix(in oklab, var(--color-ink), white 74.07%) 83.41%,
46
+ white
47
+ );
48
+ }
49
+ ```
50
+
51
+ ## Rules
52
+
53
+ - **Arguments**, each its own string: `kind` is `linear`, `radial` or `conic`; `direction` is that function's first
54
+ argument, required (`"to bottom"`, `"45deg"`, `"circle at top"`, `"at center"`, `"from 90deg"`); `from` and `to` are
55
+ colours or colour tokens; `easing` is `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out` (the default) or
56
+ `cubic-bezier(x1, y1, x2, y2)`; `stops` is 2 to 64 (13 by default). They are checked in the types and again at build
57
+ time, against one grammar.
58
+ - **Stops** are placed as postcss-easing-gradients places them: the curve's parameter `t` is sampled evenly, `x(t)` is the
59
+ position of a stop and `y(t)` the share of `to` in it. A curve that overshoots (`y` outside 0 to 1) is clamped to the two
60
+ colours.
61
+ - **Colours** are mixed in oklab with premultiplied alpha, as CSS mixes them, so `transparent` fades the other colour's hue
62
+ and does not grey it. The endpoints are printed as written; the stops between them are sRGB hex (`#rrggbbaa` when not
63
+ opaque). Positions and shares have two decimals at most, so the output is the same on every build.
64
+ - **Tokens and `currentColor`.** A token arrives as `var(--name)` and `currentColor` is the element's: neither can be read at
65
+ build time. When an endpoint cannot be, every stop is `color-mix(in oklab, from, to N%)` at the same positions, and the
66
+ browser mixes them. `color-mix()` needs Chrome 111, Safari 16.2, Firefox 113: for older targets the build warns, as it
67
+ does for any value.
68
+ - **Errors**, at the call: a part outside the grammar (`gradient: unexpected "steps(4" in "linear to bottom red blue
69
+ steps(4)", expected linear | <cubic-bezier-easing-function> | <integer> | end of value`), arguments in the wrong order
70
+ (`cssints: gradient(): "blue" is not an easing (the arguments are kind, direction, from, to, easing?, stops?)`), and a
71
+ gradient where an image does not go (`css.color(gradient(…))`, an error in the types and at build time).
72
+
73
+ ## Limits
74
+
75
+ - A stop between two literal colours is clamped to sRGB: a wide-gamut endpoint (`color(display-p3 …)`, a vivid `oklch()`)
76
+ keeps its own value at the end, but the colours between are the nearest sRGB ones. Give it as a token to keep the gamut:
77
+ the stops are then `color-mix()`.
78
+ - `steps()` and `linear()` easings are not taken, nor an easing token (there is no easing token type), nor a number token
79
+ for `stops`; a `repeating-*` gradient, more than two colours and a colour interpolation method (`in oklch`) are not
80
+ either.
81
+ - The parts are joined with a space for the grammar (see "Functions" in the API reference), so `"to", "bottom"` as two
82
+ arguments passes the grammar and is caught only by the argument check; write the direction as one string.
83
+
84
+ ```bash
85
+ cd packages/gradient && bun run test # the stop math, fixtures through the engine (output, errors)
86
+ cd packages/gradient && bun run typecheck # and the type test, test/types/gradient.ts
87
+ ```
88
+
89
+ ## Credits
90
+
91
+ The stops follow [postcss-easing-gradients](https://github.com/larsenwork/postcss-easing-gradients) and
92
+ [easing-coordinates](https://github.com/larsenwork/easing-coordinates) (MIT, Andreas Larsen): the easing curve sampled
93
+ into colour stops. The code is written here; no code is copied. Colours are mixed with [culori](https://culorijs.org)
94
+ (MIT, Dan Burzo), a dependency.
@@ -0,0 +1,20 @@
1
+ import { CSSImageValue } from "cssints";
2
+ //#region src/index.d.ts
3
+ declare const eased: import("cssints/plugin").Scope<"gradient", {
4
+ readonly linear: "linear [ <angle> | to <side-or-corner> ]";
5
+ readonly radial: "radial [ [ <radial-shape> || <radial-size> ] [ at <position> ]? | at <position> ]";
6
+ readonly conic: "conic [ from <angle> [ at <position> ]? | at <position> ]";
7
+ readonly ends: "<color> <color> [ linear | <cubic-bezier-easing-function> ]? <integer [2,64]>?";
8
+ readonly gradient: {
9
+ readonly args: "[ <linear> | <radial> | <conic> ] <ends>";
10
+ readonly value: ([kind, direction, from, to, easing, stops]: string[]) => CSSImageValue;
11
+ };
12
+ }>;
13
+ /**
14
+ * `gradient(kind, direction, from, to, easing?, stops?)`: a `linear`, `radial` or `conic` gradient from one colour to
15
+ * another along an easing (`ease-in-out` by default; `linear`, `ease*` or `cubic-bezier()`), as `stops` colour stops (13 by
16
+ * default, 2 to 64). The direction is the gradient's own (`"to bottom"`, `"45deg"`, `"circle at top"`, `"from 90deg"`). The
17
+ * stops are computed at build time in oklab; a token or `currentColor` endpoint gives `color-mix()` stops instead. An `<image>`.
18
+ */
19
+ export declare const gradient: ReturnType<typeof eased.export>["gradient"];
20
+ //#endregion
package/dist/index.mjs ADDED
@@ -0,0 +1,96 @@
1
+ import { CSSImageValue } from "cssints";
2
+ import { scope } from "cssints/plugin";
3
+ import { formatHex, formatHex8, interpolateWithPremultipliedAlpha, parse } from "culori";
4
+ //#region src/stops.ts
5
+ const NAMED = /* @__PURE__ */ new Map([
6
+ ["linear", [
7
+ 0,
8
+ 0,
9
+ 1,
10
+ 1
11
+ ]],
12
+ ["ease", [
13
+ .25,
14
+ .1,
15
+ .25,
16
+ 1
17
+ ]],
18
+ ["ease-in", [
19
+ .42,
20
+ 0,
21
+ 1,
22
+ 1
23
+ ]],
24
+ ["ease-out", [
25
+ 0,
26
+ 0,
27
+ .58,
28
+ 1
29
+ ]],
30
+ ["ease-in-out", [
31
+ .42,
32
+ 0,
33
+ .58,
34
+ 1
35
+ ]]
36
+ ]);
37
+ const curve = (easing) => {
38
+ const named = NAMED.get(easing);
39
+ if (named) return named;
40
+ const n = /^cubic-bezier\((.*)\)$/.exec(easing)?.[1]?.split(",").map(Number);
41
+ if (n?.length === 4 && n.every(Number.isFinite)) return n;
42
+ throw new Error(`cssints: gradient(): "${easing}" is not an easing (the arguments are kind, direction, from, to, easing?, stops?)`);
43
+ };
44
+ const bezier = (t, p1, p2) => 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t ** 2 * p2 + t ** 3;
45
+ /**
46
+ * A share or a position as CSS text, two decimals at most (the build output is the same on every machine).
47
+ * @param {number} n a percentage
48
+ * @returns {string} its text
49
+ */
50
+ const fixed = (n) => String(+n.toFixed(2));
51
+ /**
52
+ * The curve's parameter sampled evenly, as postcss-easing-gradients does.
53
+ * @param {string} easing `linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out` or `cubic-bezier(x1, y1, x2, y2)`
54
+ * @param {number} stops the number of colour stops, both ends included
55
+ * @returns {{ x: number, y: number }[]} per stop `x`, its position, and `y`, the share of the second colour (clamped to 0..1), both 0..1
56
+ */
57
+ const samples = (easing, stops) => {
58
+ const [x1, y1, x2, y2] = curve(easing);
59
+ return Array.from({ length: stops }, (_, i) => {
60
+ const t = i / (stops - 1);
61
+ return {
62
+ x: bezier(t, x1, x2),
63
+ y: Math.min(1, Math.max(0, bezier(t, y1, y2)))
64
+ };
65
+ });
66
+ };
67
+ /**
68
+ * `gradient(kind, direction, from, to, easing?, stops?)`: a `linear`, `radial` or `conic` gradient from one colour to
69
+ * another along an easing (`ease-in-out` by default; `linear`, `ease*` or `cubic-bezier()`), as `stops` colour stops (13 by
70
+ * default, 2 to 64). The direction is the gradient's own (`"to bottom"`, `"45deg"`, `"circle at top"`, `"from 90deg"`). The
71
+ * stops are computed at build time in oklab; a token or `currentColor` endpoint gives `color-mix()` stops instead. An `<image>`.
72
+ */
73
+ const gradient = scope("gradient", {
74
+ linear: "linear [ <angle> | to <side-or-corner> ]",
75
+ radial: "radial [ [ <radial-shape> || <radial-size> ] [ at <position> ]? | at <position> ]",
76
+ conic: "conic [ from <angle> [ at <position> ]? | at <position> ]",
77
+ ends: "<color> <color> [ linear | <cubic-bezier-easing-function> ]? <integer [2,64]>?",
78
+ gradient: {
79
+ args: "[ <linear> | <radial> | <conic> ] <ends>",
80
+ value: ([kind, direction, from = "", to = "", easing = "ease-in-out", stops = "13"]) => {
81
+ const points = samples(easing, Number(stops));
82
+ const a = parse(from);
83
+ const b = parse(to);
84
+ const mix = a && b ? interpolateWithPremultipliedAlpha([a, b], "oklab") : void 0;
85
+ const color = (y) => {
86
+ if (!mix) return `color-mix(in oklab, ${from}, ${to} ${fixed(y * 100)}%)`;
87
+ const c = mix(y);
88
+ return c.alpha === void 0 || c.alpha >= 1 ? formatHex(c) : formatHex8(c);
89
+ };
90
+ const list = points.map(({ x, y }, i) => i === 0 ? from : i === points.length - 1 ? to : `${color(y)} ${fixed(x * 100)}%`);
91
+ return new CSSImageValue(`${kind}-gradient(${direction}, ${list.join(", ")})`);
92
+ }
93
+ }
94
+ }).export().gradient;
95
+ //#endregion
96
+ export { gradient };
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@cssints/gradient",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Eased gradients for cssints: an easing curve expanded into colour stops (mixed in oklab) at build time",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/alexeiaccio/css-in-ts-compiler.git",
9
+ "directory": "packages/gradient"
10
+ },
11
+ "homepage": "https://cssints.dev",
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.mts",
20
+ "default": "./dist/index.mjs"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "culori": "^4.0.2"
25
+ },
26
+ "peerDependencies": {
27
+ "cssints": "^0.2.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }