chroma-panel 0.2.0 → 1.0.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/README.md +46 -8
- package/dist/a11y/contrast.cjs +94 -0
- package/dist/a11y/contrast.d.cts +8 -0
- package/dist/a11y/contrast.d.ts +8 -0
- package/dist/a11y/contrast.js +93 -2
- package/dist/color/css4.cjs +397 -0
- package/dist/color/css4.d.cts +10 -0
- package/dist/color/css4.d.ts +10 -0
- package/dist/color/css4.js +392 -0
- package/dist/color/parse.cjs +3 -1
- package/dist/color/parse.js +3 -1
- package/dist/color/types.d.cts +16 -0
- package/dist/color/types.d.ts +16 -0
- package/dist/color/value.cjs +11 -0
- package/dist/color/value.d.cts +4 -0
- package/dist/color/value.d.ts +4 -0
- package/dist/color/value.js +12 -0
- package/dist/color.cjs +43 -0
- package/dist/color.d.cts +9 -0
- package/dist/color.d.ts +9 -0
- package/dist/color.js +8 -0
- package/dist/components/ChromaPanel.cjs +16 -6
- package/dist/components/ChromaPanel.d.cts +4 -2
- package/dist/components/ChromaPanel.d.ts +4 -2
- package/dist/components/ChromaPanel.js +16 -6
- package/dist/components/ColorInput.cjs +1 -1
- package/dist/components/ColorInput.js +1 -1
- package/dist/components/PanelFooter.cjs +4 -4
- package/dist/components/PanelFooter.js +4 -4
- package/dist/contrast.cjs +3 -0
- package/dist/contrast.d.cts +2 -2
- package/dist/contrast.d.ts +2 -2
- package/dist/contrast.js +2 -2
- package/dist/core/store.cjs +17 -9
- package/dist/core/store.d.cts +6 -6
- package/dist/core/store.d.ts +6 -6
- package/dist/core/store.js +17 -9
- package/dist/core.cjs +10 -0
- package/dist/core.d.cts +4 -2
- package/dist/core.d.ts +4 -2
- package/dist/core.js +3 -1
- package/dist/export/tokens.cjs +39 -0
- package/dist/export/tokens.d.cts +16 -0
- package/dist/export/tokens.d.ts +16 -0
- package/dist/export/tokens.js +37 -0
- package/dist/export.cjs +6 -0
- package/dist/export.d.cts +2 -0
- package/dist/export.d.ts +2 -0
- package/dist/export.js +2 -0
- package/dist/gradient/GradientEditor.cjs +196 -0
- package/dist/gradient/GradientEditor.d.cts +17 -0
- package/dist/gradient/GradientEditor.d.ts +17 -0
- package/dist/gradient/GradientEditor.js +195 -0
- package/dist/gradient/model.cjs +63 -0
- package/dist/gradient/model.d.cts +23 -0
- package/dist/gradient/model.d.ts +23 -0
- package/dist/gradient/model.js +60 -0
- package/dist/gradient.cjs +9 -0
- package/dist/gradient.d.cts +3 -0
- package/dist/gradient.d.ts +3 -0
- package/dist/gradient.js +3 -0
- package/dist/index.cjs +13 -0
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/modes/image/index.cjs +65 -4
- package/dist/modes/image/index.d.cts +1 -0
- package/dist/modes/image/index.d.ts +1 -0
- package/dist/modes/image/index.js +65 -4
- package/dist/modes/registry.d.cts +1 -1
- package/dist/modes/registry.d.ts +1 -1
- package/dist/modes.cjs +30 -0
- package/dist/modes.d.cts +10 -0
- package/dist/modes.d.ts +10 -0
- package/dist/modes.js +17 -0
- package/dist/panel.cjs +10 -0
- package/dist/panel.d.cts +4 -2
- package/dist/panel.d.ts +4 -2
- package/dist/panel.js +3 -1
- package/dist/primitives/ChannelSlider.cjs +4 -4
- package/dist/primitives/ChannelSlider.js +4 -4
- package/dist/primitives/ColorArea.cjs +6 -6
- package/dist/primitives/ColorArea.js +6 -6
- package/dist/primitives/ColorDisc.cjs +6 -6
- package/dist/primitives/ColorDisc.js +6 -6
- package/dist/primitives/ColorField.cjs +2 -2
- package/dist/primitives/ColorField.js +2 -2
- package/dist/primitives/NumberField.cjs +2 -2
- package/dist/primitives/NumberField.js +2 -2
- package/dist/primitives/SwatchGrid.cjs +2 -2
- package/dist/primitives/SwatchGrid.js +2 -2
- package/package.json +46 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
# chroma-panel — React color picker
|
|
10
10
|
|
|
11
|
-
A lightweight, accessible React color picker component with the look and feel of the macOS color panel.
|
|
11
|
+
A lightweight, accessible React color picker component with the look and feel of the macOS color panel. Version 1 adds CSS Color 4, a gradient editor, accessible contrast suggestions and design-token exports alongside the color wheel, precise sliders, searchable swatches, image sampling, opacity and eyedropper—with zero runtime dependencies and TypeScript types included.
|
|
12
12
|
|
|
13
13
|
**[Documentation and live demo](https://chroma-panel.jscrate.dev)** · [Quick start](https://chroma-panel.jscrate.dev/react/overview/quick-start) · [Comparison](https://chroma-panel.jscrate.dev/react/overview/comparison) · [FAQ](https://chroma-panel.jscrate.dev/react/overview/faq) · [Releases](https://chroma-panel.jscrate.dev/react/overview/releases)
|
|
14
14
|
|
|
@@ -18,6 +18,9 @@ A lightweight, accessible React color picker component with the look and feel of
|
|
|
18
18
|
- Drops into a [form](https://chroma-panel.jscrate.dev/react/handbook/forms) like an `<input>`, with `name`, `required` and `form.reset()`
|
|
19
19
|
- [Accessible](https://chroma-panel.jscrate.dev/react/overview/accessibility): every color channel is a real range input, so keyboards and screen readers work
|
|
20
20
|
- No runtime dependencies, TypeScript types included, ESM and CommonJS
|
|
21
|
+
- CSS Color 4: OKLCH, OKLab, Lab, LCH and Display P3 parsing, conversion and gamut mapping
|
|
22
|
+
- A standalone, keyboard-accessible gradient editor with perceptual interpolation
|
|
23
|
+
- Alpha-aware contrast checks, accessible-color suggestions and CSS/SCSS/design-token exports
|
|
21
24
|
|
|
22
25
|
## Install
|
|
23
26
|
|
|
@@ -60,7 +63,7 @@ import { ChromaPanel } from 'chroma-panel';
|
|
|
60
63
|
|
|
61
64
|
## Pick colors from an image
|
|
62
65
|
|
|
63
|
-
The image mode takes a dropped or chosen file, shows its dominant colors as swatches, and lets you click an exact pixel through a zoom lens. The sampler behind it is exported too, for when you want the palette without the panel:
|
|
66
|
+
The image mode takes a dropped, pasted or chosen file, shows its dominant colors as swatches, and lets you click an exact pixel through a zoom lens. Palettes can be ordered by population, luminance or hue. The sampler behind it is exported too, for when you want the palette without the panel:
|
|
64
67
|
|
|
65
68
|
```ts
|
|
66
69
|
import { extractPalette } from 'chroma-panel';
|
|
@@ -73,16 +76,51 @@ It takes a `File`, a `Blob` or an image URL, validates safe size limits, and rea
|
|
|
73
76
|
|
|
74
77
|
## Smaller bundle
|
|
75
78
|
|
|
76
|
-
Importing `chroma-panel` registers all five modes. If you only need one or two, import the shell and
|
|
79
|
+
Importing `chroma-panel` registers all five modes. If you only need one or two, import the shell and pass explicit mode objects:
|
|
77
80
|
|
|
78
81
|
```tsx
|
|
79
82
|
import { ChromaPanel } from 'chroma-panel/panel';
|
|
80
|
-
import 'chroma-panel/
|
|
83
|
+
import { wheelMode } from 'chroma-panel/modes';
|
|
81
84
|
|
|
82
|
-
<ChromaPanel modes={[
|
|
85
|
+
<ChromaPanel modes={[wheelMode]} />
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
|
|
88
|
+
The original side-effect import (`import 'chroma-panel/wheel'`) remains supported. Explicit mode objects are easier for bundlers to analyze. Every mode also has its own entry point — see [entry points](https://chroma-panel.jscrate.dev/react/utils/entry-points).
|
|
89
|
+
|
|
90
|
+
## CSS Color 4 and wide gamut
|
|
91
|
+
|
|
92
|
+
The dependency-free color engine reads and converts `oklch()`, `oklab()`, `lab()`, `lch()`, `color(srgb …)` and `color(display-p3 …)`. Gamut checks do not silently clip wide-gamut input:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { parseColor, convertColor, isInGamut, mapToGamut, serializeColor } from 'chroma-panel/color';
|
|
96
|
+
|
|
97
|
+
const color = parseColor('oklch(72% 0.18 250)')!;
|
|
98
|
+
isInGamut(color, 'srgb');
|
|
99
|
+
serializeColor(mapToGamut(color, 'display-p3'));
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Gradient editor
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { GradientEditor, type GradientValue } from 'chroma-panel/gradient';
|
|
106
|
+
|
|
107
|
+
<GradientEditor defaultValue={gradient} onChangeComplete={(value) => save(value)} />
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The same entry exports `gradientToCss`, `sampleGradient`, `normalizeGradient` and `addGradientStop`. It supports linear and radial gradients, movable color stops and OKLab/OKLCH interpolation.
|
|
111
|
+
|
|
112
|
+
## Reliable change events
|
|
113
|
+
|
|
114
|
+
The original `onChange` and `onChangeComplete` callbacks remain unchanged. The new callbacks include interaction metadata:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<ColorInput
|
|
118
|
+
onValueChange={(color, meta) => preview(color.css, meta.source)}
|
|
119
|
+
onValueCommit={(color, meta) => save(color.css, meta.source)}
|
|
120
|
+
/>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`meta.source` distinguishes pointer, keyboard, field, swatch, image, eyedropper, recent-color and programmatic changes.
|
|
86
124
|
|
|
87
125
|
## Theming
|
|
88
126
|
|
|
@@ -108,8 +146,8 @@ Measured as the increase in a real Vite production build, gzipped, with React ex
|
|
|
108
146
|
|
|
109
147
|
| What you import | Added to your app |
|
|
110
148
|
| --- | --- |
|
|
111
|
-
| all five modes |
|
|
112
|
-
| shell plus one mode |
|
|
149
|
+
| all five modes | 22.9 kB |
|
|
150
|
+
| shell plus one mode | 14.9 kB |
|
|
113
151
|
|
|
114
152
|
`dependencies` is empty. `react` and `react-dom` are peer dependencies, so the copy already in your app is the one that gets used.
|
|
115
153
|
|
package/dist/a11y/contrast.cjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const require_dev = require("../core/dev.cjs");
|
|
2
2
|
const require_convert = require("../color/convert.cjs");
|
|
3
|
+
const require_css4 = require("../color/css4.cjs");
|
|
3
4
|
const require_parse = require("../color/parse.cjs");
|
|
5
|
+
const require_serialize = require("../color/serialize.cjs");
|
|
6
|
+
const require_value = require("../color/value.cjs");
|
|
4
7
|
//#region src/a11y/contrast.ts
|
|
5
8
|
function toRgb(input) {
|
|
6
9
|
if (typeof input !== "string") return require_convert.hsvaToRgba(input);
|
|
@@ -23,6 +26,94 @@ function contrastRatio(a, b) {
|
|
|
23
26
|
const dark = Math.min(la, lb);
|
|
24
27
|
return (light + .05) / (dark + .05);
|
|
25
28
|
}
|
|
29
|
+
function compositeColor(foreground, background) {
|
|
30
|
+
const fg = typeof foreground === "string" ? require_parse.parse(foreground) : foreground;
|
|
31
|
+
const bg = typeof background === "string" ? require_parse.parse(background) : background;
|
|
32
|
+
if (fg === null || bg === null) return null;
|
|
33
|
+
const f = require_convert.hsvaToRgba(fg);
|
|
34
|
+
const b = require_convert.hsvaToRgba(bg);
|
|
35
|
+
const alpha = f.a + b.a * (1 - f.a);
|
|
36
|
+
if (alpha === 0) return {
|
|
37
|
+
h: 0,
|
|
38
|
+
s: 0,
|
|
39
|
+
v: 0,
|
|
40
|
+
a: 0
|
|
41
|
+
};
|
|
42
|
+
return require_convert.rgbaToHsva({
|
|
43
|
+
r: (f.r * f.a + b.r * b.a * (1 - f.a)) / alpha,
|
|
44
|
+
g: (f.g * f.a + b.g * b.a * (1 - f.a)) / alpha,
|
|
45
|
+
b: (f.b * f.a + b.b * b.a * (1 - f.a)) / alpha,
|
|
46
|
+
a: alpha
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function contrastRatioWithAlpha(foreground, background, canvas = "#ffffff") {
|
|
50
|
+
const opaqueBackground = compositeColor(background, canvas);
|
|
51
|
+
if (opaqueBackground === null) return 1;
|
|
52
|
+
const opaqueForeground = compositeColor(foreground, opaqueBackground);
|
|
53
|
+
return opaqueForeground === null ? 1 : contrastRatio(opaqueForeground, opaqueBackground);
|
|
54
|
+
}
|
|
55
|
+
function suggestAccessibleColor(foreground, background, targetRatio = 4.5) {
|
|
56
|
+
const source = require_value.parseColor(foreground);
|
|
57
|
+
if (source === null || require_parse.parse(background) === null || !Number.isFinite(targetRatio) || targetRatio < 1) return null;
|
|
58
|
+
const currentRatio = contrastRatioWithAlpha(foreground, background);
|
|
59
|
+
if (currentRatio >= targetRatio) return {
|
|
60
|
+
color: foreground,
|
|
61
|
+
ratio: currentRatio,
|
|
62
|
+
changed: false
|
|
63
|
+
};
|
|
64
|
+
const oklch = require_css4.convertColor(source, "oklch");
|
|
65
|
+
const candidates = [];
|
|
66
|
+
for (const endpoint of [0, 1]) {
|
|
67
|
+
let passing = endpoint;
|
|
68
|
+
let failing = oklch.channels[0];
|
|
69
|
+
if (contrastRatio(toHexColor({
|
|
70
|
+
...oklch,
|
|
71
|
+
channels: [
|
|
72
|
+
endpoint,
|
|
73
|
+
oklch.channels[1],
|
|
74
|
+
oklch.channels[2]
|
|
75
|
+
],
|
|
76
|
+
alpha: 1
|
|
77
|
+
}), background) < targetRatio) continue;
|
|
78
|
+
for (let index = 0; index < 24; index++) {
|
|
79
|
+
const lightness = (passing + failing) / 2;
|
|
80
|
+
if (contrastRatio(toHexColor({
|
|
81
|
+
...oklch,
|
|
82
|
+
channels: [
|
|
83
|
+
lightness,
|
|
84
|
+
oklch.channels[1],
|
|
85
|
+
oklch.channels[2]
|
|
86
|
+
],
|
|
87
|
+
alpha: 1
|
|
88
|
+
}), background) >= targetRatio) passing = lightness;
|
|
89
|
+
else failing = lightness;
|
|
90
|
+
}
|
|
91
|
+
const color = toHexColor({
|
|
92
|
+
...oklch,
|
|
93
|
+
channels: [
|
|
94
|
+
passing,
|
|
95
|
+
oklch.channels[1],
|
|
96
|
+
oklch.channels[2]
|
|
97
|
+
],
|
|
98
|
+
alpha: 1
|
|
99
|
+
});
|
|
100
|
+
candidates.push({
|
|
101
|
+
color,
|
|
102
|
+
ratio: contrastRatio(color, background),
|
|
103
|
+
changed: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return candidates.sort((a, b) => Math.abs(require_css4.convertColor(require_value.parseColor(a.color), "oklch").channels[0] - oklch.channels[0]) - Math.abs(require_css4.convertColor(require_value.parseColor(b.color), "oklch").channels[0] - oklch.channels[0]))[0] ?? null;
|
|
107
|
+
}
|
|
108
|
+
function toHexColor(color) {
|
|
109
|
+
const srgb = require_css4.mapToGamut(color, "srgb");
|
|
110
|
+
return require_serialize.toHex(require_convert.rgbaToHsva({
|
|
111
|
+
r: srgb.channels[0] * 255,
|
|
112
|
+
g: srgb.channels[1] * 255,
|
|
113
|
+
b: srgb.channels[2] * 255,
|
|
114
|
+
a: 1
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
26
117
|
function meetsContrast(a, b, options) {
|
|
27
118
|
const { level, size = "normal" } = options;
|
|
28
119
|
const ratio = contrastRatio(a, b);
|
|
@@ -98,10 +189,13 @@ function readableTextColor(background, options = {}) {
|
|
|
98
189
|
}
|
|
99
190
|
//#endregion
|
|
100
191
|
exports.apcaContrast = apcaContrast;
|
|
192
|
+
exports.compositeColor = compositeColor;
|
|
101
193
|
exports.contrastRatio = contrastRatio;
|
|
194
|
+
exports.contrastRatioWithAlpha = contrastRatioWithAlpha;
|
|
102
195
|
exports.contrastReport = contrastReport;
|
|
103
196
|
exports.meetsContrast = meetsContrast;
|
|
104
197
|
exports.meetsNonTextContrast = meetsNonTextContrast;
|
|
105
198
|
exports.readableTextColor = readableTextColor;
|
|
106
199
|
exports.relativeLuminance = relativeLuminance;
|
|
200
|
+
exports.suggestAccessibleColor = suggestAccessibleColor;
|
|
107
201
|
exports.wcagLevel = wcagLevel;
|
package/dist/a11y/contrast.d.cts
CHANGED
|
@@ -2,6 +2,14 @@ import { Hsva } from "../color/types.cjs";
|
|
|
2
2
|
//#region src/a11y/contrast.d.ts
|
|
3
3
|
export declare function relativeLuminance(color: string | Hsva): number;
|
|
4
4
|
export declare function contrastRatio(a: string | Hsva, b: string | Hsva): number;
|
|
5
|
+
export declare function compositeColor(foreground: string | Hsva, background: string | Hsva): Hsva | null;
|
|
6
|
+
export declare function contrastRatioWithAlpha(foreground: string | Hsva, background: string | Hsva, canvas?: string | Hsva): number;
|
|
7
|
+
export interface AccessibleColorSuggestion {
|
|
8
|
+
color: string;
|
|
9
|
+
ratio: number;
|
|
10
|
+
changed: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function suggestAccessibleColor(foreground: string, background: string, targetRatio?: number): AccessibleColorSuggestion | null;
|
|
5
13
|
export type TextSize = "normal" | "large";
|
|
6
14
|
export interface ContrastOptions {
|
|
7
15
|
level: "AA" | "AAA";
|
package/dist/a11y/contrast.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { Hsva } from "../color/types.js";
|
|
|
2
2
|
//#region src/a11y/contrast.d.ts
|
|
3
3
|
export declare function relativeLuminance(color: string | Hsva): number;
|
|
4
4
|
export declare function contrastRatio(a: string | Hsva, b: string | Hsva): number;
|
|
5
|
+
export declare function compositeColor(foreground: string | Hsva, background: string | Hsva): Hsva | null;
|
|
6
|
+
export declare function contrastRatioWithAlpha(foreground: string | Hsva, background: string | Hsva, canvas?: string | Hsva): number;
|
|
7
|
+
export interface AccessibleColorSuggestion {
|
|
8
|
+
color: string;
|
|
9
|
+
ratio: number;
|
|
10
|
+
changed: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function suggestAccessibleColor(foreground: string, background: string, targetRatio?: number): AccessibleColorSuggestion | null;
|
|
5
13
|
export type TextSize = "normal" | "large";
|
|
6
14
|
export interface ContrastOptions {
|
|
7
15
|
level: "AA" | "AAA";
|
package/dist/a11y/contrast.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { warnOnce } from "../core/dev.js";
|
|
2
|
-
import { hsvaToRgba } from "../color/convert.js";
|
|
2
|
+
import { hsvaToRgba, rgbaToHsva } from "../color/convert.js";
|
|
3
|
+
import { convertColor, mapToGamut } from "../color/css4.js";
|
|
3
4
|
import { parse } from "../color/parse.js";
|
|
5
|
+
import { toHex } from "../color/serialize.js";
|
|
6
|
+
import { parseColor } from "../color/value.js";
|
|
4
7
|
//#region src/a11y/contrast.ts
|
|
5
8
|
function toRgb(input) {
|
|
6
9
|
if (typeof input !== "string") return hsvaToRgba(input);
|
|
@@ -23,6 +26,94 @@ function contrastRatio(a, b) {
|
|
|
23
26
|
const dark = Math.min(la, lb);
|
|
24
27
|
return (light + .05) / (dark + .05);
|
|
25
28
|
}
|
|
29
|
+
function compositeColor(foreground, background) {
|
|
30
|
+
const fg = typeof foreground === "string" ? parse(foreground) : foreground;
|
|
31
|
+
const bg = typeof background === "string" ? parse(background) : background;
|
|
32
|
+
if (fg === null || bg === null) return null;
|
|
33
|
+
const f = hsvaToRgba(fg);
|
|
34
|
+
const b = hsvaToRgba(bg);
|
|
35
|
+
const alpha = f.a + b.a * (1 - f.a);
|
|
36
|
+
if (alpha === 0) return {
|
|
37
|
+
h: 0,
|
|
38
|
+
s: 0,
|
|
39
|
+
v: 0,
|
|
40
|
+
a: 0
|
|
41
|
+
};
|
|
42
|
+
return rgbaToHsva({
|
|
43
|
+
r: (f.r * f.a + b.r * b.a * (1 - f.a)) / alpha,
|
|
44
|
+
g: (f.g * f.a + b.g * b.a * (1 - f.a)) / alpha,
|
|
45
|
+
b: (f.b * f.a + b.b * b.a * (1 - f.a)) / alpha,
|
|
46
|
+
a: alpha
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function contrastRatioWithAlpha(foreground, background, canvas = "#ffffff") {
|
|
50
|
+
const opaqueBackground = compositeColor(background, canvas);
|
|
51
|
+
if (opaqueBackground === null) return 1;
|
|
52
|
+
const opaqueForeground = compositeColor(foreground, opaqueBackground);
|
|
53
|
+
return opaqueForeground === null ? 1 : contrastRatio(opaqueForeground, opaqueBackground);
|
|
54
|
+
}
|
|
55
|
+
function suggestAccessibleColor(foreground, background, targetRatio = 4.5) {
|
|
56
|
+
const source = parseColor(foreground);
|
|
57
|
+
if (source === null || parse(background) === null || !Number.isFinite(targetRatio) || targetRatio < 1) return null;
|
|
58
|
+
const currentRatio = contrastRatioWithAlpha(foreground, background);
|
|
59
|
+
if (currentRatio >= targetRatio) return {
|
|
60
|
+
color: foreground,
|
|
61
|
+
ratio: currentRatio,
|
|
62
|
+
changed: false
|
|
63
|
+
};
|
|
64
|
+
const oklch = convertColor(source, "oklch");
|
|
65
|
+
const candidates = [];
|
|
66
|
+
for (const endpoint of [0, 1]) {
|
|
67
|
+
let passing = endpoint;
|
|
68
|
+
let failing = oklch.channels[0];
|
|
69
|
+
if (contrastRatio(toHexColor({
|
|
70
|
+
...oklch,
|
|
71
|
+
channels: [
|
|
72
|
+
endpoint,
|
|
73
|
+
oklch.channels[1],
|
|
74
|
+
oklch.channels[2]
|
|
75
|
+
],
|
|
76
|
+
alpha: 1
|
|
77
|
+
}), background) < targetRatio) continue;
|
|
78
|
+
for (let index = 0; index < 24; index++) {
|
|
79
|
+
const lightness = (passing + failing) / 2;
|
|
80
|
+
if (contrastRatio(toHexColor({
|
|
81
|
+
...oklch,
|
|
82
|
+
channels: [
|
|
83
|
+
lightness,
|
|
84
|
+
oklch.channels[1],
|
|
85
|
+
oklch.channels[2]
|
|
86
|
+
],
|
|
87
|
+
alpha: 1
|
|
88
|
+
}), background) >= targetRatio) passing = lightness;
|
|
89
|
+
else failing = lightness;
|
|
90
|
+
}
|
|
91
|
+
const color = toHexColor({
|
|
92
|
+
...oklch,
|
|
93
|
+
channels: [
|
|
94
|
+
passing,
|
|
95
|
+
oklch.channels[1],
|
|
96
|
+
oklch.channels[2]
|
|
97
|
+
],
|
|
98
|
+
alpha: 1
|
|
99
|
+
});
|
|
100
|
+
candidates.push({
|
|
101
|
+
color,
|
|
102
|
+
ratio: contrastRatio(color, background),
|
|
103
|
+
changed: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return candidates.sort((a, b) => Math.abs(convertColor(parseColor(a.color), "oklch").channels[0] - oklch.channels[0]) - Math.abs(convertColor(parseColor(b.color), "oklch").channels[0] - oklch.channels[0]))[0] ?? null;
|
|
107
|
+
}
|
|
108
|
+
function toHexColor(color) {
|
|
109
|
+
const srgb = mapToGamut(color, "srgb");
|
|
110
|
+
return toHex(rgbaToHsva({
|
|
111
|
+
r: srgb.channels[0] * 255,
|
|
112
|
+
g: srgb.channels[1] * 255,
|
|
113
|
+
b: srgb.channels[2] * 255,
|
|
114
|
+
a: 1
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
26
117
|
function meetsContrast(a, b, options) {
|
|
27
118
|
const { level, size = "normal" } = options;
|
|
28
119
|
const ratio = contrastRatio(a, b);
|
|
@@ -97,5 +188,5 @@ function readableTextColor(background, options = {}) {
|
|
|
97
188
|
return Math.abs(apcaContrast(dark, background)) >= Math.abs(apcaContrast(light, background)) ? dark : light;
|
|
98
189
|
}
|
|
99
190
|
//#endregion
|
|
100
|
-
export { apcaContrast, contrastRatio, contrastReport, meetsContrast, meetsNonTextContrast, readableTextColor, relativeLuminance, wcagLevel };
|
|
191
|
+
export { apcaContrast, compositeColor, contrastRatio, contrastRatioWithAlpha, contrastReport, meetsContrast, meetsNonTextContrast, readableTextColor, relativeLuminance, suggestAccessibleColor, wcagLevel };
|
|
101
192
|
|