@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +6 -3
  3. package/src/@types/react-color.d.ts +36 -0
  4. package/src/antd/{Tabs.js → Tabs.tsx} +5 -2
  5. package/src/color-filter.test.ts +24 -65
  6. package/src/color-filter.ts +62 -93
  7. package/src/color-picker/{ColorPickerOrLoader.js → ColorPickerOrLoader.tsx} +1 -1
  8. package/src/color-picker/{index.js → index.tsx} +32 -6
  9. package/src/color-utils.ts +3 -9
  10. package/src/design-system/colors-css.ts +16 -0
  11. package/src/design-system/colors.ts +23 -45
  12. package/src/design-system/def.ts +17 -0
  13. package/src/design-system/theme.ts +6 -2
  14. package/src/design-system/types.ts +102 -0
  15. package/src/design-system/vars.test.ts +9 -2
  16. package/src/design-system/vars.ts +4 -2
  17. package/src/design-system.ts +9 -1
  18. package/src/emoji-picker/emoji-picker-content-with-color.tsx +3 -3
  19. package/src/heat.ts +8 -0
  20. package/src/highlight-colors.test.ts +68 -0
  21. package/src/highlight-colors.ts +91 -0
  22. package/src/link-input/components/{AntTextAreaWithCustomReadState.js → AntTextAreaWithCustomReadState.tsx} +19 -8
  23. package/src/link-input/index.tsx +94 -0
  24. package/src/link-input/{utils.js → utils.ts} +1 -6
  25. package/src/number-input/decimal.ts +50 -0
  26. package/src/number-input/utils.ts +2 -2
  27. package/src/palette-generator.test.ts +2 -1
  28. package/src/palette-generator.ts +8 -26
  29. package/src/palettes/_.ts +2 -2
  30. package/src/palettes/common.ts +31 -0
  31. package/src/palettes/diff-colors.ts +29 -12
  32. package/src/palettes/inspect.defs.colors.neutral-arch.test.ts +72 -36
  33. package/src/palettes/inspect.defs.colors.neutral-user.test.ts +72 -36
  34. package/src/palettes/inspect.defs.colors.warm-arch.test.ts +226 -190
  35. package/src/palettes/inspect.defs.colors.warm-user.test.ts +226 -190
  36. package/src/palettes/neutral-arch.ts +3 -3
  37. package/src/palettes/neutral-user.ts +3 -3
  38. package/src/palettes/neutral.ts +4 -2
  39. package/src/palettes/warm-arch.ts +3 -3
  40. package/src/palettes/warm-user.ts +3 -3
  41. package/src/palettes/warm.ts +4 -3
  42. package/src/pretty-size.ts +5 -2
  43. package/src/root-theme-provider.test.tsx +1 -1
  44. package/src/scale-generator.ts +1 -32
  45. package/src/select/components/menu-list-virtua.tsx +127 -0
  46. package/src/select/index.tsx +1 -1
  47. package/src/select/reflection.ts +22 -0
  48. package/src/select/select.tsx +1 -0
  49. package/src/static-palettes.ts +2 -1
  50. package/src/thematic-color-picker.tsx +8 -1
  51. package/src/thematic-constants.tsx +2 -0
  52. package/src/thematic-controls.tsx +9 -1
  53. package/src/thematic-cvd.tsx +73 -0
  54. package/src/thematic-highlights.tsx +49 -0
  55. package/src/thematic-scales.tsx +8 -6
  56. package/src/thematic-state.ts +37 -16
  57. package/src/thematic.tsx +298 -305
  58. package/src/theme-provider.tsx +9 -3
  59. package/src/theme-styles.ts +2 -1
  60. package/src/link-input/index.js +0 -89
  61. package/src/number-input/decimal.js +0 -61
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @fibery/ui-kit
2
2
 
3
+ ## 2.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [d04a531]
8
+ - @fibery/helpers@2.0.1
9
+ - @fibery/react@1.5.2
10
+
3
11
  ## 2.1.0
4
12
 
5
13
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fibery/ui-kit",
3
- "version": "2.1.0",
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.0",
51
- "@fibery/react": "1.5.1"
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(function TabWrapper({children, className}, ref) {
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(tabsStyles), Tabs);
61
+ export const AntTabs = wrapWith(getWrapper(), Tabs);
@@ -7,32 +7,8 @@ describe("makeColorFilter alpha preservation", () => {
7
7
  expect(filter(input)).toMatch(/,\s*0\.5\)$/);
8
8
  };
9
9
 
10
- it("sepia + oklchL", () => {
11
- expectAlpha(makeColorFilter({sepia: 0.3, oklchL: 0.97}));
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({sepia: 0.3, oklchL: 0.97});
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({sepia: 0.3, oklchL: 0.97});
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(44, 139, 121)",
96
- "#199EE3": "rgb(53, 149, 188)",
97
- "#1FBED3": "rgb(60, 177, 181)",
98
- "#2978FB": "rgb(69, 119, 202)",
99
- "#3E53B4": "rgb(76, 86, 147)",
100
- "#4A4A4A": "rgb(78, 75, 69)",
101
- "#4FAF54": "rgb(102, 164, 90)",
102
- "#673DB6": "rgb(104, 70, 148)",
103
- "#6A849B": "rgb(120, 133, 139)",
104
- "#8EC351": "rgb(158, 188, 95)",
105
- "#99A2AB": "rgb(165, 164, 158)",
106
- "#9C2BAF": "rgb(142, 58, 144)",
107
- "#B04E31": "rgb(159, 86, 58)",
108
- "#D40915": "rgb(167, 13, 24)",
109
- "#E72065": "rgb(193, 46, 92)",
110
- "#FBA32F": "rgb(237, 168, 71)",
111
- "#FC551F": "rgb(219, 97, 47)",
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(255, 255, 255)"`);
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({sepia: 0.3, oklchL: 0.97});
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({brightness: 0.5, oklchL: 0.97})).toEqual("brightness(0.485)");
112
+ expect(cssFromFilter({oklchL: 0.97})).toEqual("brightness(0.97)");
154
113
  });
155
114
  });
@@ -1,46 +1,37 @@
1
- import {
2
- filterBrightness,
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
- brightness?: number;
20
- contrast?: number;
21
- saturate?: number;
22
- hueRotate?: number;
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
- const MODE = "rgb" as const;
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 undefined;
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 *= spec.oklchL;
62
- }
63
- if (brightness !== 1) {
64
- parts.push(`brightness(${brightness})`);
65
- }
66
-
67
- if (spec.contrast !== undefined) {
68
- parts.push(`contrast(${spec.contrast})`);
69
- }
70
- if (spec.saturate !== undefined) {
71
- parts.push(`saturate(${spec.saturate})`);
72
- }
73
- if (spec.hueRotate !== undefined) {
74
- parts.push(`hue-rotate(${spec.hueRotate}deg)`);
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(" ") : undefined;
61
+ return parts.length > 0 ? parts.join(" ") : "none";
83
62
  }
84
63
 
85
- type ColorFilter = (color: string, format?: (color: T) => string) => string;
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: T) => string = cssFromColor) => {
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
- // oxlint-disable-next-line no-console
123
- console.warn(
124
- `[deneutralize] double-deneutralize detected: "${color}" is already a deneutralize output. ` +
125
- `This likely means a color is being transformed twice — check the call site.`
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 = from(color);
87
+ const parsed = oklchFrom(color);
129
88
  if (!parsed) {
130
89
  return color;
131
90
  }
132
- let t: T = parsed;
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 = from(oklch);
109
+ t = oklchFrom(oklch);
145
110
  }
146
- for (const filter of filters) {
147
- t = filter(t);
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?.add(result);
117
+ if (__outputs && result !== color) {
118
+ __outputs.add(result);
119
+ }
151
120
  return result;
152
121
  },
153
- (color: string, format?: (color: T) => string) =>
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
- ({containerClassName, colors, getSwatchHoverColor, getColorOverrideFn = undefined, ...rest}) => {
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) : undefined}
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
- const [optimisticColor, setOptimisticColor] = useState(color);
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}
@@ -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(resolveColorArg(color, "isValidColor")) !== undefined;
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
- // eslint-disable-next-line no-param-reassign
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
- if (typeof color === "string") {
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
  }