@nextlyhq/ui 0.0.2-alpha.52 → 0.0.2-alpha.56

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/dist/color.mjs ADDED
@@ -0,0 +1,152 @@
1
+ // src/lib/color/convert.ts
2
+ var clamp01 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
3
+ var MAX_SRGB_CHROMA = 0.5;
4
+ function normalizeHue(hue) {
5
+ if (!Number.isFinite(hue)) return 0;
6
+ const wrapped = hue % 360;
7
+ return wrapped < 0 ? wrapped + 360 : wrapped;
8
+ }
9
+ function hsvToRgb({ h, s, v }) {
10
+ const hue = normalizeHue(h);
11
+ const sat = clamp01(s);
12
+ const val = clamp01(v);
13
+ const sector = hue / 60;
14
+ const chroma = val * sat;
15
+ const x = chroma * (1 - Math.abs(sector % 2 - 1));
16
+ const base = val - chroma;
17
+ let rgb;
18
+ if (sector < 1) rgb = [chroma, x, 0];
19
+ else if (sector < 2) rgb = [x, chroma, 0];
20
+ else if (sector < 3) rgb = [0, chroma, x];
21
+ else if (sector < 4) rgb = [0, x, chroma];
22
+ else if (sector < 5) rgb = [x, 0, chroma];
23
+ else rgb = [chroma, 0, x];
24
+ return { r: rgb[0] + base, g: rgb[1] + base, b: rgb[2] + base };
25
+ }
26
+ function rgbToHsv({ r, g, b }) {
27
+ const red = clamp01(r);
28
+ const green = clamp01(g);
29
+ const blue = clamp01(b);
30
+ const max = Math.max(red, green, blue);
31
+ const min = Math.min(red, green, blue);
32
+ const chroma = max - min;
33
+ let hue = 0;
34
+ if (chroma !== 0) {
35
+ if (max === red) hue = (green - blue) / chroma % 6;
36
+ else if (max === green) hue = (blue - red) / chroma + 2;
37
+ else hue = (red - green) / chroma + 4;
38
+ hue *= 60;
39
+ }
40
+ return {
41
+ h: normalizeHue(hue),
42
+ s: max === 0 ? 0 : chroma / max,
43
+ v: max
44
+ };
45
+ }
46
+ function toLinear(channel) {
47
+ return channel <= 0.04045 ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4);
48
+ }
49
+ function toGamma(channel) {
50
+ return channel <= 31308e-7 ? channel * 12.92 : 1.055 * Math.pow(channel, 1 / 2.4) - 0.055;
51
+ }
52
+ function linearRgbToOklab(r, g, b) {
53
+ const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
54
+ const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
55
+ const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
56
+ const l_ = Math.cbrt(l);
57
+ const m_ = Math.cbrt(m);
58
+ const s_ = Math.cbrt(s);
59
+ return [
60
+ 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
61
+ 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
62
+ 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
63
+ ];
64
+ }
65
+ function oklabToLinearRgb(L, a, b) {
66
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
67
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
68
+ const s_ = L - 0.0894841775 * a - 1.291485548 * b;
69
+ const l = l_ * l_ * l_;
70
+ const m = m_ * m_ * m_;
71
+ const s = s_ * s_ * s_;
72
+ return [
73
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
74
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
75
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
76
+ ];
77
+ }
78
+ function rgbToOklch({ r, g, b }) {
79
+ const [L, A, B] = linearRgbToOklab(
80
+ toLinear(clamp01(r)),
81
+ toLinear(clamp01(g)),
82
+ toLinear(clamp01(b))
83
+ );
84
+ const chroma = Math.sqrt(A * A + B * B);
85
+ const hue = chroma < 1e-6 ? 0 : normalizeHue(Math.atan2(B, A) * 180 / Math.PI);
86
+ return { l: L, c: chroma, h: hue };
87
+ }
88
+ function inGamut([r, g, b]) {
89
+ const epsilon = 0;
90
+ return r >= -epsilon && r <= 1 + epsilon && g >= -epsilon && g <= 1 + epsilon && b >= -epsilon && b <= 1 + epsilon;
91
+ }
92
+ function oklchToRgb({ l, c, h }) {
93
+ const lightness = clamp01(l);
94
+ const hueRadians = normalizeHue(h) * Math.PI / 180;
95
+ const at = (chroma) => oklabToLinearRgb(
96
+ lightness,
97
+ chroma * Math.cos(hueRadians),
98
+ chroma * Math.sin(hueRadians)
99
+ );
100
+ const requested = Math.max(0, c);
101
+ let fitting = at(requested);
102
+ if (!inGamut(fitting)) {
103
+ let low = 0;
104
+ let high = Math.min(requested, MAX_SRGB_CHROMA);
105
+ for (let i = 0; i < 20; i++) {
106
+ const mid = (low + high) / 2;
107
+ if (inGamut(at(mid))) low = mid;
108
+ else high = mid;
109
+ }
110
+ fitting = at(low);
111
+ }
112
+ return {
113
+ r: clamp01(toGamma(fitting[0])),
114
+ g: clamp01(toGamma(fitting[1])),
115
+ b: clamp01(toGamma(fitting[2]))
116
+ };
117
+ }
118
+
119
+ // src/lib/color/hex.ts
120
+ var HEX = /^#?(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
121
+ var clamp012 = (n) => n < 0 ? 0 : n > 1 ? 1 : n;
122
+ function pair(channel) {
123
+ const value = Number.isFinite(channel) ? clamp012(channel) : 0;
124
+ return Math.round(value * 255).toString(16).padStart(2, "0");
125
+ }
126
+ function parseHex(input) {
127
+ const text = input.trim();
128
+ if (!HEX.test(text)) return null;
129
+ const digits = text.replace("#", "");
130
+ const short = digits.length < 6;
131
+ const size = short ? 1 : 2;
132
+ const channel = (index) => {
133
+ const slice = digits.slice(index * size, index * size + size);
134
+ return parseInt(short ? slice + slice : slice, 16) / 255;
135
+ };
136
+ const hasAlpha = digits.length === 4 || digits.length === 8;
137
+ return {
138
+ r: channel(0),
139
+ g: channel(1),
140
+ b: channel(2),
141
+ alpha: hasAlpha ? channel(3) : 1
142
+ };
143
+ }
144
+ function toHex(color, alpha = 1) {
145
+ const opacity = Number.isFinite(alpha) ? clamp012(alpha) : 1;
146
+ const opaque = `#${pair(color.r)}${pair(color.g)}${pair(color.b)}`;
147
+ return opacity === 1 ? opaque : `${opaque}${pair(opacity)}`;
148
+ }
149
+
150
+ export { hsvToRgb, normalizeHue, oklchToRgb, parseHex, rgbToHsv, rgbToOklch, toHex };
151
+ //# sourceMappingURL=color.mjs.map
152
+ //# sourceMappingURL=color.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/color/convert.ts","../src/lib/color/hex.ts"],"names":["clamp01"],"mappings":"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","file":"color.mjs","sourcesContent":["/**\n * Conversions between the colour models an editing surface needs.\n *\n * ## Why each model is here\n *\n * - **sRGB** is what a screen displays and what `#rrggbb` encodes. Everything ends here.\n * - **HSV** is what a colour picker is: a square of saturation against value, beside a hue\n * strip. No other model puts those three controls in the shape people expect.\n * - **OKLCH** is what this product's own theme is written in — every token in `theme.css` is an\n * `oklch()` value. A picker that cannot read it cannot edit the theme.\n *\n * ## Why the maths is here rather than in a library\n *\n * These are fixed, published transforms: the sRGB transfer function and the OKLab matrices are\n * specified in CSS Color 4 and do not change. The package ships no runtime colour dependency, and\n * this module keeps it that way. `culori` remains a DEV dependency used as a test oracle — the\n * conversions here are cross-checked against it rather than trusted on their own.\n *\n * ## The part that is genuinely hard\n *\n * OKLCH describes more colours than a screen can show. Converting one that falls outside sRGB has\n * no correct answer, only choices, and the naive choice — clamp each channel independently — is\n * the wrong one: it shifts hue, because clipping red without clipping green changes the ratio\n * between them. {@link oklchToRgb} reduces chroma instead, holding lightness and hue, which is the\n * approach CSS Color 4 describes for gamut mapping.\n *\n * @module lib/color/convert\n */\n\n/**\n * An sRGB colour with channels in [0, 1]. Alpha is carried separately.\n *\n * @experimental\n */\nexport interface Rgb {\n r: number;\n g: number;\n b: number;\n}\n\n/**\n * Hue in [0, 360), saturation and value in [0, 1].\n *\n * @experimental\n */\nexport interface Hsv {\n h: number;\n s: number;\n v: number;\n}\n\n/**\n * Perceptual lightness in [0, 1], chroma from 0, hue in [0, 360).\n *\n * @experimental\n */\nexport interface Oklch {\n l: number;\n c: number;\n h: number;\n}\n\nconst clamp01 = (n: number): number => (n < 0 ? 0 : n > 1 ? 1 : n);\n\n/**\n * A chroma no displayable sRGB colour exceeds.\n *\n * The most saturated sRGB primaries sit near 0.37 in OKLCH; this leaves headroom above that and\n * gives the gamut search a fixed bracket, so its error is absolute rather than proportional to\n * whatever the caller asked for.\n */\nconst MAX_SRGB_CHROMA = 0.5;\n\n/**\n * Wrap a hue into [0, 360), so -30 and 330 are the same angle.\n *\n * @experimental\n */\nexport function normalizeHue(hue: number): number {\n if (!Number.isFinite(hue)) return 0;\n const wrapped = hue % 360;\n return wrapped < 0 ? wrapped + 360 : wrapped;\n}\n\n/**\n * HSV to sRGB.\n * Saturation and value are clamped rather than rejected: a picker drags them, and a drag that\n * overshoots by a rounding error should saturate rather than throw.\n *\n * @experimental\n */\nexport function hsvToRgb({ h, s, v }: Hsv): Rgb {\n const hue = normalizeHue(h);\n const sat = clamp01(s);\n const val = clamp01(v);\n\n const sector = hue / 60;\n const chroma = val * sat;\n // The second-largest component, which falls as the hue moves away from a primary.\n const x = chroma * (1 - Math.abs((sector % 2) - 1));\n const base = val - chroma;\n\n let rgb: [number, number, number];\n if (sector < 1) rgb = [chroma, x, 0];\n else if (sector < 2) rgb = [x, chroma, 0];\n else if (sector < 3) rgb = [0, chroma, x];\n else if (sector < 4) rgb = [0, x, chroma];\n else if (sector < 5) rgb = [x, 0, chroma];\n else rgb = [chroma, 0, x];\n\n return { r: rgb[0] + base, g: rgb[1] + base, b: rgb[2] + base };\n}\n\n/**\n * sRGB to HSV.\n * Hue is UNDEFINED for a grey and value is undefined for black, and this returns 0 for both. A\n * picker must not take that 0 as the user's hue: it is the absence of one. Holding hue across a\n * drag to zero saturation is the caller's job, which is why a picker keeps HSV as its state\n * rather than deriving it from the colour each render.\n *\n * @experimental\n */\nexport function rgbToHsv({ r, g, b }: Rgb): Hsv {\n const red = clamp01(r);\n const green = clamp01(g);\n const blue = clamp01(b);\n\n const max = Math.max(red, green, blue);\n const min = Math.min(red, green, blue);\n const chroma = max - min;\n\n let hue = 0;\n if (chroma !== 0) {\n if (max === red) hue = ((green - blue) / chroma) % 6;\n else if (max === green) hue = (blue - red) / chroma + 2;\n else hue = (red - green) / chroma + 4;\n hue *= 60;\n }\n\n return {\n h: normalizeHue(hue),\n s: max === 0 ? 0 : chroma / max,\n v: max,\n };\n}\n\n/** The sRGB transfer function, gamma-encoded to linear light. */\nfunction toLinear(channel: number): number {\n return channel <= 0.04045\n ? channel / 12.92\n : Math.pow((channel + 0.055) / 1.055, 2.4);\n}\n\n/** The inverse transfer function, linear light back to gamma-encoded. */\nfunction toGamma(channel: number): number {\n return channel <= 0.0031308\n ? channel * 12.92\n : 1.055 * Math.pow(channel, 1 / 2.4) - 0.055;\n}\n\n/**\n * Linear sRGB to OKLab.\n *\n * The two matrices and the cube root between them are the definition of OKLab, given in CSS\n * Color 4. They are transcribed rather than derived.\n */\nfunction linearRgbToOklab(\n r: number,\n g: number,\n b: number\n): [number, number, number] {\n const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;\n const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;\n const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;\n\n const l_ = Math.cbrt(l);\n const m_ = Math.cbrt(m);\n const s_ = Math.cbrt(s);\n\n return [\n 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,\n 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,\n 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,\n ];\n}\n\n/** OKLab back to linear sRGB, the inverse of {@link linearRgbToOklab}. */\nfunction oklabToLinearRgb(\n L: number,\n a: number,\n b: number\n): [number, number, number] {\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n const s_ = L - 0.0894841775 * a - 1.291485548 * b;\n\n const l = l_ * l_ * l_;\n const m = m_ * m_ * m_;\n const s = s_ * s_ * s_;\n\n return [\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n ];\n}\n\n/**\n * sRGB to OKLCH.\n *\n * @experimental\n */\nexport function rgbToOklch({ r, g, b }: Rgb): Oklch {\n const [L, A, B] = linearRgbToOklab(\n toLinear(clamp01(r)),\n toLinear(clamp01(g)),\n toLinear(clamp01(b))\n );\n const chroma = Math.sqrt(A * A + B * B);\n // Below this the hue angle is numerical noise rather than a colour, so it is reported as 0\n // instead of whatever direction the rounding happened to point.\n const hue =\n chroma < 1e-6 ? 0 : normalizeHue((Math.atan2(B, A) * 180) / Math.PI);\n return { l: L, c: chroma, h: hue };\n}\n\n/** Whether every channel of a linear triple lies within the displayable range. */\nfunction inGamut([r, g, b]: [number, number, number]): boolean {\n // No tolerance at all: the point must be GENUINELY inside the gamut.\n //\n // Any fixed tolerance is eventually comparable to the channel values themselves, because those\n // shrink without bound as lightness falls — 1e-6 broke the hue at l=0.01, and 1e-12 still broke\n // it at l=0.0001. A tolerance exists to absorb the cube roots' error for a colour sitting\n // exactly ON the boundary, and the cost of not absorbing it is that such a colour comes back\n // with its chroma reduced by less than one part in a million, which nothing can display. The\n // cost of absorbing it is a channel that must then be clamped, and clamping is precisely what\n // moves the hue this function promises to keep.\n const epsilon = 0;\n return (\n r >= -epsilon &&\n r <= 1 + epsilon &&\n g >= -epsilon &&\n g <= 1 + epsilon &&\n b >= -epsilon &&\n b <= 1 + epsilon\n );\n}\n\n/**\n * OKLCH to sRGB, reducing chroma until the colour fits on screen.\n * OKLCH can name colours a display cannot show. Clamping the channels independently is the\n * obvious response and the wrong one: clipping red without clipping green changes the ratio\n * between them, so the colour that appears is a DIFFERENT HUE from the one asked for. Lightness\n * and hue are what a person chose; chroma is the part they will not miss, so chroma is what is\n * given up.\n * The search is a bisection on chroma, which converges to well under a perceptible step in the\n * fixed number of rounds below — no loop that might not terminate.\n *\n * **The gamut is not always monotonic along this ray, and that is a deliberate trade.** At some\n * lightness and hue pairs the boundary is crossed more than once: at `l = 0.2, h = 264.1` the ray\n * is inside up to c ≈ 0.1192, outside until c ≈ 0.1368, briefly inside again to c ≈ 0.1386. A\n * bisection therefore finds the FIRST boundary rather than the outermost reachable chroma.\n *\n * That is the intended behaviour rather than a limitation. Those islands span roughly a tenth of\n * a degree of hue, so preferring the outermost value would make chroma jump by about 0.021\n * between h = 264.05 and h = 264.10 — a visible step while dragging a hue slider, over a change\n * no one can aim at. Taking the first boundary gives 0.1160, 0.1175, 0.1192, 0.1255, 0.1383\n * across the same sweep: continuous, and one step less saturated at worst. It is also what the\n * CSS Color 4 gamut-mapping algorithm does.\n *\n * @experimental\n */\nexport function oklchToRgb({ l, c, h }: Oklch): Rgb {\n const lightness = clamp01(l);\n const hueRadians = (normalizeHue(h) * Math.PI) / 180;\n\n const at = (chroma: number): [number, number, number] =>\n oklabToLinearRgb(\n lightness,\n chroma * Math.cos(hueRadians),\n chroma * Math.sin(hueRadians)\n );\n\n const requested = Math.max(0, c);\n let fitting = at(requested);\n\n if (!inGamut(fitting)) {\n let low = 0;\n // Bracketed against an ABSOLUTE ceiling rather than the requested chroma. Halving an\n // unbounded input a fixed number of times converges on a fraction OF THAT INPUT, so a\n // sufficiently large chroma leaves the interval far above the gamut and the search returns\n // zero — discarding the hue entirely and answering grey for a colour that has one. No sRGB\n // colour exceeds a chroma of roughly 0.37, so this ceiling cannot exclude a reachable one.\n let high = Math.min(requested, MAX_SRGB_CHROMA);\n // Halvings of a fixed interval, so the remaining error is absolute: 0.5 / 2^20 is far below\n // anything a screen or an eye resolves.\n for (let i = 0; i < 20; i++) {\n const mid = (low + high) / 2;\n if (inGamut(at(mid))) low = mid;\n else high = mid;\n }\n fitting = at(low);\n }\n\n return {\n r: clamp01(toGamma(fitting[0])),\n g: clamp01(toGamma(fitting[1])),\n b: clamp01(toGamma(fitting[2])),\n };\n}\n","/**\n * Hex is the notation people type, so it is the picker's front door.\n *\n * It lives beside the conversions rather than in the component that reads it, for the same reason\n * they do: parsing `#3b82f6` is arithmetic on a string, a server rendering a token swatch needs it\n * as much as an editing surface does, and pulling it into client code would put it behind the\n * `\"use client\"` boundary for no benefit.\n *\n * @module lib/color/hex\n */\n\nimport type { Rgb } from \"./convert\";\n\n/**\n * An sRGB colour with its alpha, which is what a hex string can carry and {@link Rgb} cannot.\n *\n * `alpha` is in [0, 1] like the channels, rather than 0-255, so it composes with the conversions\n * without a second unit in play.\n *\n * @experimental\n */\nexport interface Rgba extends Rgb {\n alpha: number;\n}\n\n/** `#rgb`, `#rgba`, `#rrggbb` or `#rrggbbaa`, with the hash optional and case ignored. */\nconst HEX = /^#?(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;\n\nconst clamp01 = (n: number): number => (n < 0 ? 0 : n > 1 ? 1 : n);\n\n/** A channel byte as a two-digit lowercase pair. */\nfunction pair(channel: number): string {\n // A non-finite channel becomes 0 rather than passing through. `clamp01(NaN)` is NaN, because\n // both of its comparisons are false, and formatting that yields the three characters \"NaN\" —\n // producing `#NaN0000`, which is exactly the string this module documents itself as never\n // returning.\n const value = Number.isFinite(channel) ? clamp01(channel) : 0;\n return Math.round(value * 255)\n .toString(16)\n .padStart(2, \"0\");\n}\n\n/**\n * Read a hex colour, or `null` if the input is not one.\n *\n * Returns `null` rather than throwing or substituting black: this reads what someone is part-way\n * through typing, where \"not a colour yet\" is the ordinary case and neither an exception nor a\n * silent black is a useful answer. A caller decides whether to hold the last good value, show a\n * message, or wait.\n *\n * Both short forms are accepted because both are what people paste. `#abc` expands by REPEATING\n * each digit rather than padding with zero — `#abc` is `#aabbcc`, not `#a0b0c0` — which is what CSS\n * does and the only expansion under which `#fff` is white.\n *\n * @experimental\n */\nexport function parseHex(input: string): Rgba | null {\n const text = input.trim();\n if (!HEX.test(text)) return null;\n\n const digits = text.replace(\"#\", \"\");\n const short = digits.length < 6;\n const size = short ? 1 : 2;\n const channel = (index: number): number => {\n const slice = digits.slice(index * size, index * size + size);\n return parseInt(short ? slice + slice : slice, 16) / 255;\n };\n\n const hasAlpha = digits.length === 4 || digits.length === 8;\n return {\n r: channel(0),\n g: channel(1),\n b: channel(2),\n alpha: hasAlpha ? channel(3) : 1,\n };\n}\n\n/**\n * Write a colour as `#rrggbb`, or `#rrggbbaa` when it is not fully opaque.\n *\n * The alpha pair is omitted at 1 rather than always written, because `#3b82f6ff` is the same colour\n * and the shorter form is what someone expects to see in a field they typed `#3b82f6` into.\n *\n * Channels outside [0, 1] are clamped, so the result is always a valid six- or eight-digit hex\n * string whatever a caller passes. Not for floating-point drift, which rounds to the right byte on\n * its own: it is for a value that is wrong by a lot — a channel still in 0-255, or a computation\n * that overshot — where the alternative is emitting `#17f00-80`, a string nothing downstream can\n * parse and no error explains.\n *\n * @experimental\n */\nexport function toHex(color: Rgb, alpha = 1): string {\n // A non-finite alpha falls back to 1, not to 0 as a channel does. Both are \"nothing was\n // specified\", and for alpha that is this parameter's own default — where treating it as 0 would\n // turn a colour invisible on a stray NaN, silently and with no way to tell from the output.\n const opacity = Number.isFinite(alpha) ? clamp01(alpha) : 1;\n const opaque = `#${pair(color.r)}${pair(color.g)}${pair(color.b)}`;\n return opacity === 1 ? opaque : `${opaque}${pair(opacity)}`;\n}\n"]}