@fibery/ui-kit 2.1.0 → 2.1.1
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/CHANGELOG.md +8 -0
- package/package.json +6 -3
- package/src/@types/react-color.d.ts +36 -0
- package/src/antd/{Tabs.js → Tabs.tsx} +5 -2
- package/src/color-filter.test.ts +24 -65
- package/src/color-filter.ts +62 -93
- package/src/color-picker/{ColorPickerOrLoader.js → ColorPickerOrLoader.tsx} +1 -1
- package/src/color-picker/{index.js → index.tsx} +32 -6
- package/src/color-utils.ts +3 -9
- package/src/design-system/colors-css.ts +16 -0
- package/src/design-system/colors.ts +23 -45
- package/src/design-system/def.ts +17 -0
- package/src/design-system/theme.ts +6 -2
- package/src/design-system/types.ts +102 -0
- package/src/design-system/vars.test.ts +9 -2
- package/src/design-system/vars.ts +4 -2
- package/src/design-system.ts +9 -1
- package/src/emoji-picker/emoji-picker-content-with-color.tsx +3 -3
- package/src/heat.ts +8 -0
- package/src/highlight-colors.test.ts +68 -0
- package/src/highlight-colors.ts +91 -0
- package/src/link-input/components/{AntTextAreaWithCustomReadState.js → AntTextAreaWithCustomReadState.tsx} +19 -8
- package/src/link-input/index.tsx +94 -0
- package/src/link-input/{utils.js → utils.ts} +1 -6
- package/src/number-input/decimal.ts +50 -0
- package/src/number-input/utils.ts +2 -2
- package/src/palette-generator.test.ts +2 -1
- package/src/palette-generator.ts +8 -26
- package/src/palettes/_.ts +2 -2
- package/src/palettes/common.ts +31 -0
- package/src/palettes/diff-colors.ts +29 -12
- package/src/palettes/inspect.defs.colors.neutral-arch.test.ts +72 -36
- package/src/palettes/inspect.defs.colors.neutral-user.test.ts +72 -36
- package/src/palettes/inspect.defs.colors.warm-arch.test.ts +226 -190
- package/src/palettes/inspect.defs.colors.warm-user.test.ts +226 -190
- package/src/palettes/neutral-arch.ts +3 -3
- package/src/palettes/neutral-user.ts +3 -3
- package/src/palettes/neutral.ts +4 -2
- package/src/palettes/warm-arch.ts +3 -3
- package/src/palettes/warm-user.ts +3 -3
- package/src/palettes/warm.ts +4 -3
- package/src/pretty-size.ts +5 -2
- package/src/root-theme-provider.test.tsx +1 -1
- package/src/scale-generator.ts +1 -32
- package/src/select/components/menu-list-virtua.tsx +127 -0
- package/src/select/index.tsx +1 -1
- package/src/select/reflection.ts +22 -0
- package/src/select/select.tsx +1 -0
- package/src/static-palettes.ts +2 -1
- package/src/thematic-color-picker.tsx +8 -1
- package/src/thematic-constants.tsx +2 -0
- package/src/thematic-controls.tsx +9 -1
- package/src/thematic-cvd.tsx +73 -0
- package/src/thematic-highlights.tsx +49 -0
- package/src/thematic-scales.tsx +8 -6
- package/src/thematic-state.ts +37 -16
- package/src/thematic.tsx +298 -305
- package/src/theme-provider.tsx +9 -3
- package/src/theme-styles.ts +2 -1
- package/src/link-input/index.js +0 -89
- package/src/number-input/decimal.js +0 -61
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fibery/ui-kit",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"dependencies": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"@types/react-select-country-list": "^2.2.3",
|
|
22
22
|
"@types/setimmediate": "^1.0.4",
|
|
23
23
|
"antd": "4.24.7",
|
|
24
|
+
"apcach": "github:antiflasher/apcach#54077b3",
|
|
24
25
|
"chrono-node": "^2.9.0",
|
|
25
26
|
"classnames": "2.3.1",
|
|
26
27
|
"cmdk": "1.0.0",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"date-fns": "2.29.2",
|
|
31
32
|
"invariant": "2.2.4",
|
|
32
33
|
"lodash": "4.17.21",
|
|
34
|
+
"lru-cache": "11.2.2",
|
|
33
35
|
"md5": "2.2.1",
|
|
34
36
|
"moment": "2.29.4",
|
|
35
37
|
"photoswipe": "5.4.4",
|
|
@@ -46,9 +48,10 @@
|
|
|
46
48
|
"react-virtuoso": "4.18.1",
|
|
47
49
|
"screenfull": "6.0.2",
|
|
48
50
|
"tabbable": "5.2.1",
|
|
51
|
+
"virtua": "0.48.8",
|
|
49
52
|
"@fibery/emoji-data": "2.7.3",
|
|
50
|
-
"@fibery/helpers": "2.0.
|
|
51
|
-
"@fibery/react": "1.5.
|
|
53
|
+
"@fibery/helpers": "2.0.1",
|
|
54
|
+
"@fibery/react": "1.5.2"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
57
|
"@babel/core": "7.28.5",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
declare module "react-color/lib/components/common" {
|
|
2
|
+
import type {ComponentType} from "react";
|
|
3
|
+
|
|
4
|
+
// ColorWrap HOC: injects hex/hsl/hsv/rgb/onChange into the inner component,
|
|
5
|
+
// and exposes color/onChangeComplete/onChange on the outer component.
|
|
6
|
+
// Internally, onChange/onChangeComplete receive a full ColorResult object
|
|
7
|
+
// (ColorWrap.handleChange calls toState() before forwarding).
|
|
8
|
+
type ColorResult = {
|
|
9
|
+
hex: string;
|
|
10
|
+
hsl: {h: number; s: number; l: number; a: number};
|
|
11
|
+
hsv: {h: number; s: number; v: number; a: number};
|
|
12
|
+
rgb: {r: number; g: number; b: number; a: number};
|
|
13
|
+
oldHue: number;
|
|
14
|
+
source: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type ColorWrapExternalProps = {
|
|
18
|
+
color?: string;
|
|
19
|
+
onChangeComplete?: (color: ColorResult) => void;
|
|
20
|
+
onChange?: (color: ColorResult) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ColorWrap overrides inner onChange with its own handler that passes ColorResult.
|
|
24
|
+
// Omit onChange from inner props to avoid union conflict.
|
|
25
|
+
export function ColorWrap<P extends Record<string, unknown>>(
|
|
26
|
+
component: ComponentType<P>
|
|
27
|
+
): ComponentType<Omit<Partial<P>, "onChange"> & ColorWrapExternalProps>;
|
|
28
|
+
|
|
29
|
+
export const EditableInput: ComponentType<{
|
|
30
|
+
style?: Record<string, Record<string, unknown>>;
|
|
31
|
+
value?: string;
|
|
32
|
+
onChange?: (color: string) => void;
|
|
33
|
+
}>;
|
|
34
|
+
export const Hue: ComponentType<Record<string, unknown>>;
|
|
35
|
+
export const Saturation: ComponentType<Record<string, unknown>>;
|
|
36
|
+
}
|
|
@@ -46,7 +46,10 @@ export const tabsStyles = css`
|
|
|
46
46
|
`;
|
|
47
47
|
|
|
48
48
|
function getWrapper() {
|
|
49
|
-
return forwardRef
|
|
49
|
+
return forwardRef<HTMLDivElement, {children: React.ReactNode; className?: string}>(function TabWrapper(
|
|
50
|
+
{children, className},
|
|
51
|
+
ref
|
|
52
|
+
) {
|
|
50
53
|
return (
|
|
51
54
|
<div className={cx(className, tabsStyles)} ref={ref}>
|
|
52
55
|
{children}
|
|
@@ -55,4 +58,4 @@ function getWrapper() {
|
|
|
55
58
|
});
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
export const AntTabs = wrapWith(getWrapper(
|
|
61
|
+
export const AntTabs = wrapWith(getWrapper(), Tabs);
|
package/src/color-filter.test.ts
CHANGED
|
@@ -7,32 +7,8 @@ describe("makeColorFilter alpha preservation", () => {
|
|
|
7
7
|
expect(filter(input)).toMatch(/,\s*0\.5\)$/);
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
it("
|
|
11
|
-
expectAlpha(makeColorFilter({
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("brightness", () => {
|
|
15
|
-
expectAlpha(makeColorFilter({brightness: 1.2}));
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("contrast", () => {
|
|
19
|
-
expectAlpha(makeColorFilter({contrast: 0.8}));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("saturate", () => {
|
|
23
|
-
expectAlpha(makeColorFilter({saturate: 1.5}));
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("hueRotate", () => {
|
|
27
|
-
expectAlpha(makeColorFilter({hueRotate: 45}));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("grayscale", () => {
|
|
31
|
-
expectAlpha(makeColorFilter({grayscale: 0.5}));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("invert", () => {
|
|
35
|
-
expectAlpha(makeColorFilter({invert: 1}));
|
|
10
|
+
it("oklchL", () => {
|
|
11
|
+
expectAlpha(makeColorFilter({oklchL: 0.97}));
|
|
36
12
|
});
|
|
37
13
|
|
|
38
14
|
it("oklchC", () => {
|
|
@@ -46,15 +22,8 @@ describe("makeColorFilter alpha preservation", () => {
|
|
|
46
22
|
it("all filters combined", () => {
|
|
47
23
|
expectAlpha(
|
|
48
24
|
makeColorFilter({
|
|
49
|
-
sepia: 0.3,
|
|
50
25
|
oklchL: 0.97,
|
|
51
26
|
oklchC: 0.8,
|
|
52
|
-
brightness: 1.1,
|
|
53
|
-
contrast: 0.9,
|
|
54
|
-
saturate: 1.2,
|
|
55
|
-
hueRotate: 10,
|
|
56
|
-
grayscale: 0.1,
|
|
57
|
-
invert: 0.05,
|
|
58
27
|
hueShift: {targetHue: 200, amount: 0.3},
|
|
59
28
|
})
|
|
60
29
|
);
|
|
@@ -70,7 +39,7 @@ describe("makeColorFilter identity", () => {
|
|
|
70
39
|
});
|
|
71
40
|
|
|
72
41
|
describe("makeColorFilter null/empty color", () => {
|
|
73
|
-
const filter = makeColorFilter({
|
|
42
|
+
const filter = makeColorFilter({oklchL: 0.97});
|
|
74
43
|
|
|
75
44
|
it("does not throw on null color", () => {
|
|
76
45
|
expect(filter(null as unknown as string)).toBeNull();
|
|
@@ -86,29 +55,29 @@ describe("makeColorFilter null/empty color", () => {
|
|
|
86
55
|
});
|
|
87
56
|
|
|
88
57
|
describe("makeColorFilter warm deneutralize (sepia + oklchL)", () => {
|
|
89
|
-
const warmFilter = makeColorFilter({
|
|
58
|
+
const warmFilter = makeColorFilter({oklchL: 0.97});
|
|
90
59
|
|
|
91
60
|
it("transforms cardTypeColors snapshot", () => {
|
|
92
61
|
const transformed = Object.fromEntries(cardTypeColors.map((c) => [c, warmFilter(c)]));
|
|
93
62
|
expect(transformed).toMatchInlineSnapshot(`
|
|
94
63
|
{
|
|
95
|
-
"#159789": "rgb(
|
|
96
|
-
"#199EE3": "rgb(
|
|
97
|
-
"#1FBED3": "rgb(
|
|
98
|
-
"#2978FB": "rgb(
|
|
99
|
-
"#3E53B4": "rgb(
|
|
100
|
-
"#4A4A4A": "rgb(
|
|
101
|
-
"#4FAF54": "rgb(
|
|
102
|
-
"#673DB6": "rgb(
|
|
103
|
-
"#6A849B": "rgb(
|
|
104
|
-
"#8EC351": "rgb(
|
|
105
|
-
"#99A2AB": "rgb(
|
|
106
|
-
"#9C2BAF": "rgb(
|
|
107
|
-
"#B04E31": "rgb(
|
|
108
|
-
"#D40915": "rgb(
|
|
109
|
-
"#E72065": "rgb(
|
|
110
|
-
"#FBA32F": "rgb(
|
|
111
|
-
"#FC551F": "rgb(
|
|
64
|
+
"#159789": "rgb(3, 145, 132)",
|
|
65
|
+
"#199EE3": "rgb(6, 152, 220)",
|
|
66
|
+
"#1FBED3": "rgb(8, 183, 204)",
|
|
67
|
+
"#2978FB": "rgb(35, 114, 245)",
|
|
68
|
+
"#3E53B4": "rgb(58, 79, 175)",
|
|
69
|
+
"#4A4A4A": "rgb(71, 71, 71)",
|
|
70
|
+
"#4FAF54": "rgb(72, 169, 78)",
|
|
71
|
+
"#673DB6": "rgb(99, 56, 177)",
|
|
72
|
+
"#6A849B": "rgb(101, 127, 149)",
|
|
73
|
+
"#8EC351": "rgb(135, 188, 73)",
|
|
74
|
+
"#99A2AB": "rgb(147, 155, 164)",
|
|
75
|
+
"#9C2BAF": "rgb(151, 37, 170)",
|
|
76
|
+
"#B04E31": "rgb(171, 73, 44)",
|
|
77
|
+
"#D40915": "rgb(206, 0, 13)",
|
|
78
|
+
"#E72065": "rgb(224, 19, 96)",
|
|
79
|
+
"#FBA32F": "rgb(243, 155, 35)",
|
|
80
|
+
"#FC551F": "rgb(245, 78, 20)",
|
|
112
81
|
}
|
|
113
82
|
`);
|
|
114
83
|
});
|
|
@@ -121,14 +90,14 @@ describe("makeColorFilter warm deneutralize (sepia + oklchL)", () => {
|
|
|
121
90
|
const black = warmFilter("#000000");
|
|
122
91
|
const white = warmFilter("#ffffff");
|
|
123
92
|
expect(black).toMatchInlineSnapshot(`"rgb(0, 0, 0)"`);
|
|
124
|
-
expect(white).toMatchInlineSnapshot(`"rgb(
|
|
93
|
+
expect(white).toMatchInlineSnapshot(`"rgb(245, 245, 245)"`);
|
|
125
94
|
});
|
|
126
95
|
});
|
|
127
96
|
|
|
128
97
|
describe("makeColorFilter double-deneutralize guard", () => {
|
|
129
98
|
it("warns when applying filter to an already-filtered output", () => {
|
|
130
99
|
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
131
|
-
const filter = makeColorFilter({
|
|
100
|
+
const filter = makeColorFilter({oklchL: 0.97});
|
|
132
101
|
|
|
133
102
|
const first = filter("#D40915");
|
|
134
103
|
filter(first);
|
|
@@ -136,20 +105,10 @@ describe("makeColorFilter double-deneutralize guard", () => {
|
|
|
136
105
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("double-deneutralize detected"));
|
|
137
106
|
warnSpy.mockRestore();
|
|
138
107
|
});
|
|
139
|
-
|
|
140
|
-
it("does not warn on first application", () => {
|
|
141
|
-
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
142
|
-
const filter = makeColorFilter({sepia: 0.1});
|
|
143
|
-
|
|
144
|
-
filter("#00ff00");
|
|
145
|
-
|
|
146
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
147
|
-
warnSpy.mockRestore();
|
|
148
|
-
});
|
|
149
108
|
});
|
|
150
109
|
|
|
151
110
|
describe("cssFromFilter", () => {
|
|
152
111
|
it("approximates oklchL via brightness", () => {
|
|
153
|
-
expect(cssFromFilter({
|
|
112
|
+
expect(cssFromFilter({oklchL: 0.97})).toEqual("brightness(0.97)");
|
|
154
113
|
});
|
|
155
114
|
});
|
package/src/color-filter.ts
CHANGED
|
@@ -1,46 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
filterContrast,
|
|
4
|
-
filterGrayscale,
|
|
5
|
-
filterHueRotate,
|
|
6
|
-
filterInvert,
|
|
7
|
-
filterSaturate,
|
|
8
|
-
filterSepia,
|
|
9
|
-
Rgb,
|
|
10
|
-
} from "culori";
|
|
11
|
-
import {cssFromColor, oklchFrom, rgbFrom} from "./color-utils";
|
|
1
|
+
import {filterDeficiencyDeuter, filterDeficiencyProt, filterDeficiencyTrit, type Oklch} from "culori";
|
|
2
|
+
import {cssFromColor, oklchFrom} from "./color-utils";
|
|
12
3
|
import {shiftHueTowards} from "./palette-generator";
|
|
13
4
|
import memoize from "lodash/memoize";
|
|
5
|
+
import {adjustHeat} from "./heat";
|
|
6
|
+
|
|
7
|
+
export const cvdChannels = [
|
|
8
|
+
{key: "cvdProtan", label: "protan (red-blind)", filter: filterDeficiencyProt},
|
|
9
|
+
{key: "cvdDeutan", label: "deutan (green-blind)", filter: filterDeficiencyDeuter},
|
|
10
|
+
{key: "cvdTritan", label: "tritan (blue-blind)", filter: filterDeficiencyTrit},
|
|
11
|
+
] as const;
|
|
14
12
|
|
|
15
13
|
export type ColorFilterSpec = {
|
|
16
14
|
hueShift?: {targetHue: number; amount: number};
|
|
17
15
|
oklchL?: number;
|
|
18
16
|
oklchC?: number;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
sepia?: number;
|
|
24
|
-
grayscale?: number;
|
|
25
|
-
invert?: number;
|
|
17
|
+
heat?: number; // -1..1 cold..hot
|
|
18
|
+
cvdProtan?: number;
|
|
19
|
+
cvdDeutan?: number;
|
|
20
|
+
cvdTritan?: number;
|
|
26
21
|
};
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
type T = Rgb;
|
|
30
|
-
const from = rgbFrom;
|
|
31
|
-
|
|
32
|
-
export function cssFromFilter(spec?: ColorFilterSpec): string | undefined {
|
|
23
|
+
export function cssFromFilter(spec?: ColorFilterSpec): string {
|
|
33
24
|
if (!spec) {
|
|
34
|
-
return
|
|
25
|
+
return "none";
|
|
35
26
|
}
|
|
36
27
|
if (process.env.NODE_ENV !== "production") {
|
|
37
28
|
const unsupported: string[] = [];
|
|
38
|
-
if (spec.oklchC !== undefined) {
|
|
39
|
-
unsupported.push("oklchC (no CSS equivalent for chroma adjustment)");
|
|
40
|
-
}
|
|
41
29
|
if (spec.hueShift) {
|
|
42
30
|
unsupported.push("hueShift (CSS hue-rotate is uniform, hueShift targets a specific hue)");
|
|
43
31
|
}
|
|
32
|
+
if (spec.cvdProtan || spec.cvdDeutan || spec.cvdTritan) {
|
|
33
|
+
unsupported.push("cvd (color vision deficiency simulation has no CSS equivalent)");
|
|
34
|
+
}
|
|
44
35
|
if (unsupported.length > 0) {
|
|
45
36
|
// oxlint-disable-next-line no-console
|
|
46
37
|
console.warn(
|
|
@@ -50,86 +41,60 @@ export function cssFromFilter(spec?: ColorFilterSpec): string | undefined {
|
|
|
50
41
|
}
|
|
51
42
|
}
|
|
52
43
|
const parts: string[] = [];
|
|
53
|
-
if (spec.sepia !== undefined) {
|
|
54
|
-
parts.push(`sepia(${spec.sepia})`);
|
|
55
|
-
}
|
|
56
|
-
let brightness = 1;
|
|
57
|
-
if (spec.brightness !== undefined) {
|
|
58
|
-
brightness *= spec.brightness;
|
|
59
|
-
}
|
|
60
44
|
if (spec.oklchL !== undefined) {
|
|
61
|
-
brightness
|
|
62
|
-
}
|
|
63
|
-
if (
|
|
64
|
-
parts.push(`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (spec.grayscale !== undefined) {
|
|
77
|
-
parts.push(`grayscale(${spec.grayscale})`);
|
|
78
|
-
}
|
|
79
|
-
if (spec.invert !== undefined) {
|
|
80
|
-
parts.push(`invert(${spec.invert})`);
|
|
45
|
+
parts.push(`brightness(${spec.oklchL})`);
|
|
46
|
+
}
|
|
47
|
+
if (spec.oklchC !== undefined) {
|
|
48
|
+
parts.push(`saturate(${spec.oklchC})`);
|
|
49
|
+
}
|
|
50
|
+
if (spec.heat !== undefined && spec.heat !== 0) {
|
|
51
|
+
if (spec.heat > 0) {
|
|
52
|
+
// approximate warm shift (oklch mix towards hue 29) via sepia + saturate
|
|
53
|
+
const t = spec.heat;
|
|
54
|
+
parts.push(`sepia(${(t * 0.35).toFixed(3)})`);
|
|
55
|
+
parts.push(`saturate(${(1 + t * 0.4).toFixed(3)})`);
|
|
56
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
57
|
+
// oxlint-disable-next-line no-console
|
|
58
|
+
console.warn("[cssFromFilter] negative temperature (cold shift) has no CSS filter approximation");
|
|
59
|
+
}
|
|
81
60
|
}
|
|
82
|
-
return parts.length > 0 ? parts.join(" ") :
|
|
61
|
+
return parts.length > 0 ? parts.join(" ") : "none";
|
|
83
62
|
}
|
|
84
63
|
|
|
85
|
-
type ColorFilter = (color: string, format?: (color:
|
|
64
|
+
type ColorFilter = (color: string, format?: (color: Oklch) => string) => string;
|
|
86
65
|
|
|
87
66
|
export function makeColorFilter(spec?: ColorFilterSpec): ColorFilter {
|
|
88
67
|
if (!spec) {
|
|
89
68
|
return (x) => x;
|
|
90
69
|
}
|
|
91
|
-
type ColorToSameColorMapper = ReturnType<typeof filterBrightness>;
|
|
92
|
-
const filters: ColorToSameColorMapper[] = [];
|
|
93
|
-
if (spec.brightness !== undefined) {
|
|
94
|
-
filters.push(filterBrightness(spec.brightness, MODE));
|
|
95
|
-
}
|
|
96
|
-
if (spec.contrast !== undefined) {
|
|
97
|
-
filters.push(filterContrast(spec.contrast, MODE));
|
|
98
|
-
}
|
|
99
|
-
if (spec.saturate !== undefined) {
|
|
100
|
-
filters.push(filterSaturate(spec.saturate, MODE));
|
|
101
|
-
}
|
|
102
|
-
if (spec.hueRotate !== undefined) {
|
|
103
|
-
filters.push(filterHueRotate(spec.hueRotate, MODE));
|
|
104
|
-
}
|
|
105
|
-
if (spec.sepia !== undefined) {
|
|
106
|
-
filters.push(filterSepia(spec.sepia, MODE));
|
|
107
|
-
}
|
|
108
|
-
if (spec.grayscale !== undefined) {
|
|
109
|
-
filters.push(filterGrayscale(spec.grayscale, MODE));
|
|
110
|
-
}
|
|
111
|
-
if (spec.invert !== undefined) {
|
|
112
|
-
filters.push(filterInvert(spec.invert, MODE));
|
|
113
|
-
}
|
|
114
70
|
const __outputs = process.env.NODE_ENV !== "production" ? new Set<string>() : undefined;
|
|
115
|
-
|
|
71
|
+
const __reported = process.env.NODE_ENV !== "production" ? new Set<string>() : undefined;
|
|
116
72
|
return memoize(
|
|
117
|
-
(color: string, format: (color:
|
|
73
|
+
(color: string, format: (color: Oklch) => string = cssFromColor) => {
|
|
118
74
|
if (!color) {
|
|
119
75
|
return color;
|
|
120
76
|
}
|
|
121
77
|
if (__outputs?.has(color)) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
78
|
+
if (!__reported?.has(color)) {
|
|
79
|
+
__reported?.add(color);
|
|
80
|
+
// oxlint-disable-next-line no-console
|
|
81
|
+
console.warn(
|
|
82
|
+
`[deneutralize] double-deneutralize detected: "${color}" is already a deneutralize output. ` +
|
|
83
|
+
`This likely means a color is being transformed twice — check the call site.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
127
86
|
}
|
|
128
|
-
const parsed =
|
|
87
|
+
const parsed = oklchFrom(color);
|
|
129
88
|
if (!parsed) {
|
|
130
89
|
return color;
|
|
131
90
|
}
|
|
132
|
-
let t:
|
|
91
|
+
let t: Oklch = parsed;
|
|
92
|
+
for (const {key, filter} of cvdChannels) {
|
|
93
|
+
const severity = spec[key];
|
|
94
|
+
if (severity) {
|
|
95
|
+
t = filter(severity)(t);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
133
98
|
if (spec.hueShift || spec.oklchL !== undefined || spec.oklchC !== undefined) {
|
|
134
99
|
const oklch = oklchFrom(t);
|
|
135
100
|
if (spec.hueShift) {
|
|
@@ -141,16 +106,20 @@ export function makeColorFilter(spec?: ColorFilterSpec): ColorFilter {
|
|
|
141
106
|
if (spec.oklchC !== undefined) {
|
|
142
107
|
oklch.c = (oklch.c ?? 0) * spec.oklchC;
|
|
143
108
|
}
|
|
144
|
-
t =
|
|
109
|
+
t = oklchFrom(oklch);
|
|
145
110
|
}
|
|
146
|
-
|
|
147
|
-
|
|
111
|
+
|
|
112
|
+
if (spec.heat !== undefined && spec.heat !== 0) {
|
|
113
|
+
t = adjustHeat(t, spec.heat);
|
|
148
114
|
}
|
|
115
|
+
|
|
149
116
|
const result = format(t);
|
|
150
|
-
__outputs
|
|
117
|
+
if (__outputs && result !== color) {
|
|
118
|
+
__outputs.add(result);
|
|
119
|
+
}
|
|
151
120
|
return result;
|
|
152
121
|
},
|
|
153
|
-
(color: string, format?: (color:
|
|
122
|
+
(color: string, format?: (color: Oklch) => string) =>
|
|
154
123
|
format === cssFromColor || format === undefined ? color : `${color}\0${format?.name || format.toString()}`
|
|
155
124
|
);
|
|
156
125
|
}
|
|
@@ -16,7 +16,7 @@ const LazyColorPicker = lazy(async () => {
|
|
|
16
16
|
return {default: ColorPicker};
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
export const ColorPickerOrLoader = (props) => (
|
|
19
|
+
export const ColorPickerOrLoader = (props: React.ComponentProps<typeof LazyColorPicker>) => (
|
|
20
20
|
<Suspense fallback={<div className={colorPickerLoaderStyle}>Loading color picker component…</div>}>
|
|
21
21
|
<LazyColorPicker {...props} />
|
|
22
22
|
</Suspense>
|
|
@@ -31,8 +31,21 @@ const hexInpuWrapperCss = css`
|
|
|
31
31
|
margin-top: ${space.s12}px;
|
|
32
32
|
`;
|
|
33
33
|
|
|
34
|
+
type SliderPickerOwnProps = {
|
|
35
|
+
containerClassName: string;
|
|
36
|
+
colors?: readonly string[];
|
|
37
|
+
getSwatchHoverColor?: (color: string) => string | undefined;
|
|
38
|
+
getColorOverrideFn?: (color: string) => string;
|
|
39
|
+
};
|
|
40
|
+
|
|
34
41
|
const SliderPicker = ColorWrap(
|
|
35
|
-
({
|
|
42
|
+
({
|
|
43
|
+
containerClassName,
|
|
44
|
+
colors,
|
|
45
|
+
getSwatchHoverColor,
|
|
46
|
+
getColorOverrideFn,
|
|
47
|
+
...rest
|
|
48
|
+
}: SliderPickerOwnProps & {hex: string; color: string; onChange: (color: string) => void}) => {
|
|
36
49
|
const hasSelectedColor = Boolean(rest.color);
|
|
37
50
|
|
|
38
51
|
const idxSelectedColor = colors?.findIndex((color) => color.toLowerCase() === rest.hex.toLowerCase());
|
|
@@ -40,7 +53,7 @@ const SliderPicker = ColorWrap(
|
|
|
40
53
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
41
54
|
|
|
42
55
|
const isCustomSwatchSelected = (idxSelectedColor === -1 && hasSelectedColor) || isExpanded;
|
|
43
|
-
const getColorOverride = getColorOverrideFn ?? ((color) => color);
|
|
56
|
+
const getColorOverride = getColorOverrideFn ?? ((color: string) => color);
|
|
44
57
|
|
|
45
58
|
return (
|
|
46
59
|
<CollapsibleRoot open={isCustomSwatchSelected} onOpenChange={setIsExpanded} className={pickerWrapperCss}>
|
|
@@ -65,7 +78,7 @@ const SliderPicker = ColorWrap(
|
|
|
65
78
|
getHoverColor={getSwatchHoverColor}
|
|
66
79
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
67
80
|
selected={isCustomSwatchSelected}
|
|
68
|
-
color={isCustomSwatchSelected ? getColorOverride(rest.hex) :
|
|
81
|
+
color={isCustomSwatchSelected ? getColorOverride(rest.hex) : ""}
|
|
69
82
|
/>
|
|
70
83
|
</CollapsibleTrigger>
|
|
71
84
|
</div>
|
|
@@ -102,6 +115,18 @@ const SliderPicker = ColorWrap(
|
|
|
102
115
|
}
|
|
103
116
|
);
|
|
104
117
|
|
|
118
|
+
export type ColorPickerProps = {
|
|
119
|
+
color: string;
|
|
120
|
+
getSwatchHoverColor?: (color: string) => string | undefined;
|
|
121
|
+
getColorOverrideFn?: (color: string) => string;
|
|
122
|
+
colors?: readonly string[];
|
|
123
|
+
// ColorWrap passes {hex, hsl, rgb, oldHue, ...} — consumers access .hex
|
|
124
|
+
onChangeComplete: (color: {hex: string} & Record<string, unknown>) => void;
|
|
125
|
+
containerClassName?: string;
|
|
126
|
+
// ColorWrap forwards unknown props to the inner component
|
|
127
|
+
[key: string]: unknown;
|
|
128
|
+
};
|
|
129
|
+
|
|
105
130
|
export const ColorPicker = ({
|
|
106
131
|
color,
|
|
107
132
|
getSwatchHoverColor,
|
|
@@ -109,13 +134,14 @@ export const ColorPicker = ({
|
|
|
109
134
|
colors,
|
|
110
135
|
onChangeComplete,
|
|
111
136
|
containerClassName = colorSwatchContainerCss,
|
|
112
|
-
}) => {
|
|
113
|
-
|
|
137
|
+
}: ColorPickerProps) => {
|
|
138
|
+
// ColorWrap onChange passes {hex, hsl, rgb, ...}; getDerivedStateFromProps handles both string and object
|
|
139
|
+
const [optimisticColor, setOptimisticColor] = useState<unknown>(color);
|
|
114
140
|
|
|
115
141
|
return (
|
|
116
142
|
<SliderPicker
|
|
117
143
|
containerClassName={containerClassName}
|
|
118
|
-
color={optimisticColor}
|
|
144
|
+
color={optimisticColor as string}
|
|
119
145
|
getSwatchHoverColor={getSwatchHoverColor}
|
|
120
146
|
getColorOverrideFn={getColorOverrideFn}
|
|
121
147
|
colors={colors}
|
package/src/color-utils.ts
CHANGED
|
@@ -94,12 +94,10 @@ export function setAlpha(color: Color, value: number): void {
|
|
|
94
94
|
color.alpha = value;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
export const isValidColor = (color: string) => parse(
|
|
97
|
+
export const isValidColor = (color: string) => parse(color) !== undefined;
|
|
98
98
|
|
|
99
99
|
export function colorFromUnsafe(color: string, fallback = "hsl(220, 62%, 56%)"): Color {
|
|
100
|
-
|
|
101
|
-
color = resolveColorArg(color, "colorFromUnsafe");
|
|
102
|
-
const parsed = parse(color) ?? parse(fallback)!;
|
|
100
|
+
const parsed = parse(resolveColorArg(color, "colorFromUnsafe") ?? "") ?? parse(fallback)!;
|
|
103
101
|
if (!parsed) {
|
|
104
102
|
throw new Error(`Failed to parse fallback color ${fallback}`);
|
|
105
103
|
}
|
|
@@ -164,11 +162,7 @@ export function cssFromUnsafe(color: string): string {
|
|
|
164
162
|
export function serializeHexVariadic(color: Rgb): string;
|
|
165
163
|
export function serializeHexVariadic(color: string): string | undefined;
|
|
166
164
|
export function serializeHexVariadic(color: string | Rgb): string | undefined {
|
|
167
|
-
|
|
168
|
-
// eslint-disable-next-line no-param-reassign
|
|
169
|
-
color = resolveColorArg(color, "serializeHexVariadic");
|
|
170
|
-
}
|
|
171
|
-
const rgb = rgbFrom(color);
|
|
165
|
+
const rgb = rgbFrom(typeof color === "string" ? resolveColorArg(color, "serializeHexVariadic") : color);
|
|
172
166
|
if (!rgb) {
|
|
173
167
|
return rgb;
|
|
174
168
|
}
|
|
@@ -12,6 +12,22 @@ export function getEnumTextColor(color: string) {
|
|
|
12
12
|
return `color-mix(in srgb, ${color} 40%, ${themeVars.textColor} 60%)`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Border on filled background swatches */
|
|
16
|
+
export function getBgSwatchBorderColor(fill: string) {
|
|
17
|
+
return `light-dark(
|
|
18
|
+
oklch(from ${fill} calc(l * 0.9) calc(c * 1.6) h),
|
|
19
|
+
oklch(from ${fill} calc(l * 1.5) calc(c * 1.5) h)
|
|
20
|
+
)`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Opaque fill for palette swatches */
|
|
24
|
+
export function getBgSwatchFillColor(fill: string) {
|
|
25
|
+
return `light-dark(
|
|
26
|
+
oklch(from ${fill} calc(l * 0.98) calc(c * 1.25) h),
|
|
27
|
+
oklch(from ${fill} calc(l * 1.25) calc(c * 1.65) h)
|
|
28
|
+
)`;
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
function themeVarWithAlpha(themeVar: string, alpha: number) {
|
|
16
32
|
return `color-mix(in srgb, ${themeVar} ${Math.round(alpha * 100)}%, transparent)`;
|
|
17
33
|
}
|