@darthrapid/react-native-color-picker 0.1.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.
Files changed (65) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +160 -0
  3. package/lib/module/components/color-picker.js +214 -0
  4. package/lib/module/components/color-picker.js.map +1 -0
  5. package/lib/module/components/hue-strip.js +113 -0
  6. package/lib/module/components/hue-strip.js.map +1 -0
  7. package/lib/module/components/picker-panel.js +121 -0
  8. package/lib/module/components/picker-panel.js.map +1 -0
  9. package/lib/module/components/picker-tab.js +38 -0
  10. package/lib/module/components/picker-tab.js.map +1 -0
  11. package/lib/module/components/recent-tab.js +89 -0
  12. package/lib/module/components/recent-tab.js.map +1 -0
  13. package/lib/module/components/sat-bright-pad.js +171 -0
  14. package/lib/module/components/sat-bright-pad.js.map +1 -0
  15. package/lib/module/components/tab-bar.js +41 -0
  16. package/lib/module/components/tab-bar.js.map +1 -0
  17. package/lib/module/components/values-tab.js +161 -0
  18. package/lib/module/components/values-tab.js.map +1 -0
  19. package/lib/module/index.js +4 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/module/shared/const.js +43 -0
  23. package/lib/module/shared/const.js.map +1 -0
  24. package/lib/module/types/misc.js +4 -0
  25. package/lib/module/types/misc.js.map +1 -0
  26. package/lib/module/utils/colors.js +52 -0
  27. package/lib/module/utils/colors.js.map +1 -0
  28. package/lib/typescript/package.json +1 -0
  29. package/lib/typescript/src/components/color-picker.d.ts +37 -0
  30. package/lib/typescript/src/components/color-picker.d.ts.map +1 -0
  31. package/lib/typescript/src/components/hue-strip.d.ts +10 -0
  32. package/lib/typescript/src/components/hue-strip.d.ts.map +1 -0
  33. package/lib/typescript/src/components/picker-panel.d.ts +32 -0
  34. package/lib/typescript/src/components/picker-panel.d.ts.map +1 -0
  35. package/lib/typescript/src/components/picker-tab.d.ts +17 -0
  36. package/lib/typescript/src/components/picker-tab.d.ts.map +1 -0
  37. package/lib/typescript/src/components/recent-tab.d.ts +12 -0
  38. package/lib/typescript/src/components/recent-tab.d.ts.map +1 -0
  39. package/lib/typescript/src/components/sat-bright-pad.d.ts +14 -0
  40. package/lib/typescript/src/components/sat-bright-pad.d.ts.map +1 -0
  41. package/lib/typescript/src/components/tab-bar.d.ts +11 -0
  42. package/lib/typescript/src/components/tab-bar.d.ts.map +1 -0
  43. package/lib/typescript/src/components/values-tab.d.ts +17 -0
  44. package/lib/typescript/src/components/values-tab.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +3 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/lib/typescript/src/shared/const.d.ts +33 -0
  48. package/lib/typescript/src/shared/const.d.ts.map +1 -0
  49. package/lib/typescript/src/types/misc.d.ts +21 -0
  50. package/lib/typescript/src/types/misc.d.ts.map +1 -0
  51. package/lib/typescript/src/utils/colors.d.ts +14 -0
  52. package/lib/typescript/src/utils/colors.d.ts.map +1 -0
  53. package/package.json +123 -0
  54. package/src/components/color-picker.tsx +290 -0
  55. package/src/components/hue-strip.tsx +134 -0
  56. package/src/components/picker-panel.tsx +176 -0
  57. package/src/components/picker-tab.tsx +48 -0
  58. package/src/components/recent-tab.tsx +90 -0
  59. package/src/components/sat-bright-pad.tsx +158 -0
  60. package/src/components/tab-bar.tsx +59 -0
  61. package/src/components/values-tab.tsx +166 -0
  62. package/src/index.tsx +2 -0
  63. package/src/shared/const.ts +43 -0
  64. package/src/types/misc.ts +24 -0
  65. package/src/utils/colors.ts +57 -0
@@ -0,0 +1,90 @@
1
+ import {
2
+ Pressable,
3
+ Text,
4
+ View
5
+ } from "react-native";
6
+ import type { ColorPickerLabels, Theme } from "../types/misc";
7
+
8
+ type RecentTabProps = {
9
+ recentColors: string[];
10
+ disabled: boolean;
11
+ t: Theme;
12
+ labels: Required<ColorPickerLabels>;
13
+ onSelect: (hex: string) => void;
14
+ onClear: () => void;
15
+ }
16
+
17
+ export function RecentTab({
18
+ recentColors,
19
+ disabled,
20
+ t,
21
+ labels,
22
+ onSelect,
23
+ onClear,
24
+ }: RecentTabProps) {
25
+ if (recentColors.length === 0) {
26
+ return (
27
+ <View
28
+ style={{
29
+ padding: 20,
30
+ alignItems: "center",
31
+ justifyContent: "center",
32
+ minHeight: 120,
33
+ }}
34
+ >
35
+ <Text style={{ color: t.textDim, fontSize: 14 }}>
36
+ {labels.noSavedColors}
37
+ </Text>
38
+ <Text style={{ color: t.textDim, fontSize: 12, marginTop: 4 }}>
39
+ {labels.noSavedColorsHint}
40
+ </Text>
41
+ </View>
42
+ );
43
+ }
44
+
45
+ return (
46
+ <View style={{ padding: 20, gap: 16 }}>
47
+ <View
48
+ style={{
49
+ flexDirection: "row",
50
+ justifyContent: "space-between",
51
+ alignItems: "center",
52
+ }}
53
+ >
54
+ <Text
55
+ style={{
56
+ color: t.textMuted,
57
+ fontSize: 11,
58
+ fontWeight: "700",
59
+ letterSpacing: 1,
60
+ textTransform: "uppercase",
61
+ }}
62
+ >
63
+ {labels.savedColors}
64
+ </Text>
65
+ <Pressable onPress={onClear}>
66
+ <Text style={{ color: t.textDim, fontSize: 12, fontWeight: "600" }}>
67
+ {labels.clearAll}
68
+ </Text>
69
+ </Pressable>
70
+ </View>
71
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 10 }}>
72
+ {recentColors.map((c, i) => (
73
+ <Pressable
74
+ key={`${c}-${i}`}
75
+ onPress={() => onSelect(c)}
76
+ disabled={disabled}
77
+ style={{
78
+ width: 44,
79
+ height: 44,
80
+ borderRadius: 10,
81
+ backgroundColor: c,
82
+ borderWidth: 1,
83
+ borderColor: t.border,
84
+ }}
85
+ />
86
+ ))}
87
+ </View>
88
+ </View>
89
+ );
90
+ }
@@ -0,0 +1,158 @@
1
+ import React, {
2
+ useCallback,
3
+ useRef,
4
+ useState
5
+ } from "react";
6
+ import {
7
+ PanResponder,
8
+ View
9
+ } from "react-native";
10
+ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
11
+ import { hsbToHex } from "../utils/colors";
12
+
13
+ type SatBrightPadProps = {
14
+ hue: number;
15
+ sat: number;
16
+ bright: number;
17
+ disabled: boolean;
18
+ thumbBorder: string;
19
+ onChange: (vals: { s: number; b: number }) => void;
20
+ }
21
+
22
+ export function SatBrightPad({
23
+ hue,
24
+ sat,
25
+ bright,
26
+ disabled,
27
+ thumbBorder,
28
+ onChange,
29
+ }: SatBrightPadProps) {
30
+ const containerRef = useRef<React.ComponentRef<typeof View>>(null);
31
+ const originRef = useRef<{ x: number; y: number } | null>(null);
32
+ const sizeRef = useRef(0);
33
+ const disabledRef = useRef(disabled);
34
+ const onChangeRef = useRef(onChange);
35
+ disabledRef.current = disabled;
36
+ onChangeRef.current = onChange;
37
+
38
+ const [layoutSize, setLayoutSize] = useState(0);
39
+
40
+ const calcValues = useCallback((pageX: number, pageY: number) => {
41
+ const s = sizeRef.current;
42
+ if (!originRef.current || s === 0) return null;
43
+ const x = Math.max(0, Math.min(pageX - originRef.current.x, s));
44
+ const y = Math.max(0, Math.min(pageY - originRef.current.y, s));
45
+ return {
46
+ s: Math.round((x / s) * 100),
47
+ b: Math.round(100 - (y / s) * 100),
48
+ };
49
+ }, []);
50
+
51
+ const panResponder = useRef(
52
+ PanResponder.create({
53
+ onStartShouldSetPanResponder: () => !disabledRef.current,
54
+ onStartShouldSetPanResponderCapture: () => !disabledRef.current,
55
+ onMoveShouldSetPanResponder: () => !disabledRef.current,
56
+ onMoveShouldSetPanResponderCapture: () => !disabledRef.current,
57
+ onPanResponderTerminationRequest: () => false,
58
+ onPanResponderGrant: (evt) => {
59
+ const { pageX, pageY } = evt.nativeEvent;
60
+ containerRef.current?.measure((_x: number, _y: number, w: number, _h: number, ox: number, oy: number) => {
61
+ originRef.current = { x: ox, y: oy };
62
+ sizeRef.current = w;
63
+ const vals = calcValues(pageX, pageY);
64
+ if (vals) onChangeRef.current(vals);
65
+ });
66
+ },
67
+ onPanResponderMove: (evt) => {
68
+ const { pageX, pageY } = evt.nativeEvent;
69
+ const vals = calcValues(pageX, pageY);
70
+ if (vals) onChangeRef.current(vals);
71
+ },
72
+ }),
73
+ ).current;
74
+
75
+ const thumbX = (sat / 100) * layoutSize;
76
+ const thumbY = ((100 - bright) / 100) * layoutSize;
77
+ const currentColor = hsbToHex(hue, sat, bright);
78
+ const hueColor = hsbToHex(hue, 100, 100);
79
+
80
+ return (
81
+ <View
82
+ ref={containerRef}
83
+ onLayout={(e) => {
84
+ const w = e.nativeEvent.layout.width;
85
+ setLayoutSize(w);
86
+ sizeRef.current = w;
87
+ }}
88
+ style={{
89
+ width: "100%",
90
+ aspectRatio: 1,
91
+ borderRadius: 12,
92
+ overflow: "hidden",
93
+ }}
94
+ {...panResponder.panHandlers}
95
+ >
96
+ {layoutSize > 0 && (
97
+ <>
98
+ <Svg
99
+ width={layoutSize}
100
+ height={layoutSize}
101
+ style={{ position: "absolute", top: 0, left: 0 }}
102
+ >
103
+ <Defs>
104
+ <LinearGradient id="grad_sat" x1="0" y1="0" x2="1" y2="0">
105
+ <Stop offset="0" stopColor="#FFFFFF" stopOpacity={1} />
106
+ <Stop offset="1" stopColor={hueColor} stopOpacity={1} />
107
+ </LinearGradient>
108
+ </Defs>
109
+ <Rect
110
+ x="0"
111
+ y="0"
112
+ width={layoutSize}
113
+ height={layoutSize}
114
+ fill="url(#grad_sat)"
115
+ />
116
+ </Svg>
117
+ <Svg
118
+ width={layoutSize}
119
+ height={layoutSize}
120
+ style={{ position: "absolute", top: 0, left: 0 }}
121
+ >
122
+ <Defs>
123
+ <LinearGradient id="grad_bright" x1="0" y1="0" x2="0" y2="1">
124
+ <Stop offset="0" stopColor="#000000" stopOpacity={0} />
125
+ <Stop offset="1" stopColor="#000000" stopOpacity={1} />
126
+ </LinearGradient>
127
+ </Defs>
128
+ <Rect
129
+ x="0"
130
+ y="0"
131
+ width={layoutSize}
132
+ height={layoutSize}
133
+ fill="url(#grad_bright)"
134
+ />
135
+ </Svg>
136
+ <View
137
+ style={{
138
+ position: "absolute",
139
+ left: thumbX - 12,
140
+ top: thumbY - 12,
141
+ width: 24,
142
+ height: 24,
143
+ borderRadius: 12,
144
+ borderWidth: 3,
145
+ borderColor: thumbBorder,
146
+ backgroundColor: currentColor,
147
+ shadowColor: "#000",
148
+ shadowOffset: { width: 0, height: 2 },
149
+ shadowOpacity: 0.4,
150
+ shadowRadius: 4,
151
+ elevation: 5,
152
+ }}
153
+ />
154
+ </>
155
+ )}
156
+ </View>
157
+ );
158
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ Pressable,
3
+ Text,
4
+ View
5
+ } from "react-native";
6
+ import type { ColorPickerLabels, TabId, Theme } from "../types/misc";
7
+
8
+ type TabBarProps = {
9
+ tabs: TabId[];
10
+ active: TabId;
11
+ onSelect: (tab: TabId) => void;
12
+ t: Theme;
13
+ labels: Required<ColorPickerLabels>;
14
+ }
15
+
16
+ export function TabBar({
17
+ tabs,
18
+ active,
19
+ onSelect,
20
+ t,
21
+ labels,
22
+ }: TabBarProps) {
23
+ return (
24
+ <View
25
+ style={{
26
+ flexDirection: "row",
27
+ borderBottomWidth: 1,
28
+ borderBottomColor: t.border,
29
+ }}
30
+ >
31
+ {tabs.map((tab) => {
32
+ const isActive = tab === active;
33
+ return (
34
+ <Pressable
35
+ key={tab}
36
+ onPress={() => onSelect(tab)}
37
+ style={{
38
+ flex: 1,
39
+ paddingVertical: 12,
40
+ alignItems: "center",
41
+ borderBottomWidth: 2,
42
+ borderBottomColor: isActive ? t.tabIndicator : "transparent",
43
+ }}
44
+ >
45
+ <Text
46
+ style={{
47
+ fontSize: 13,
48
+ fontWeight: "600",
49
+ color: isActive ? t.tabActive : t.tabInactive,
50
+ }}
51
+ >
52
+ {labels[tab]}
53
+ </Text>
54
+ </Pressable>
55
+ );
56
+ })}
57
+ </View>
58
+ );
59
+ }
@@ -0,0 +1,166 @@
1
+ import {
2
+ Text,
3
+ TextInput,
4
+ View
5
+ } from "react-native";
6
+ import type { Theme } from "../types/misc";
7
+ import { hexToRgb } from "../utils/colors";
8
+
9
+ type ValuesTabProps = {
10
+ hue: number;
11
+ sat: number;
12
+ bright: number;
13
+ currentHex: string;
14
+ hexInput: string;
15
+ disabled: boolean;
16
+ t: Theme;
17
+ onHexInputChange: (text: string) => void;
18
+ onHexSubmit: () => void;
19
+ onHexInputFocus: () => void;
20
+ onHexInputBlur: () => void;
21
+ }
22
+
23
+ export function ValuesTab({
24
+ hue,
25
+ sat,
26
+ bright,
27
+ currentHex,
28
+ hexInput,
29
+ disabled,
30
+ t,
31
+ onHexInputChange,
32
+ onHexSubmit,
33
+ onHexInputFocus,
34
+ onHexInputBlur,
35
+ }: ValuesTabProps) {
36
+ const rgb = hexToRgb(currentHex);
37
+
38
+ return (
39
+ <View style={{ padding: 20, gap: 16 }}>
40
+ {/* Hex input */}
41
+ <View style={{ flexDirection: "row", gap: 8 }}>
42
+ <TextInput
43
+ value={hexInput}
44
+ onChangeText={onHexInputChange}
45
+ onBlur={() => {
46
+ onHexInputBlur();
47
+ onHexSubmit();
48
+ }}
49
+ onFocus={onHexInputFocus}
50
+ onSubmitEditing={onHexSubmit}
51
+ maxLength={7}
52
+ autoCapitalize="characters"
53
+ autoCorrect={false}
54
+ editable={!disabled}
55
+ style={{
56
+ flex: 1,
57
+ backgroundColor: t.inputBg,
58
+ borderWidth: 1,
59
+ borderColor: t.border,
60
+ borderRadius: 10,
61
+ paddingHorizontal: 14,
62
+ paddingVertical: 12,
63
+ color: t.text,
64
+ fontSize: 16,
65
+ fontWeight: "500",
66
+ fontVariant: ["tabular-nums"],
67
+ }}
68
+ />
69
+ <View
70
+ style={{
71
+ width: 48,
72
+ height: 48,
73
+ borderRadius: 10,
74
+ backgroundColor: currentHex,
75
+ borderWidth: 1,
76
+ borderColor: t.border,
77
+ }}
78
+ />
79
+ </View>
80
+
81
+ {/* RGB */}
82
+ <View style={{ flexDirection: "row", gap: 8 }}>
83
+ {[
84
+ { label: "R", value: rgb.r, color: "#FF6B6B" },
85
+ { label: "G", value: rgb.g, color: "#51CF66" },
86
+ { label: "B", value: rgb.b, color: "#339AF0" },
87
+ ].map(({ label, value: val, color }) => (
88
+ <View
89
+ key={label}
90
+ style={{
91
+ flex: 1,
92
+ backgroundColor: t.surface,
93
+ borderRadius: 10,
94
+ paddingVertical: 10,
95
+ alignItems: "center",
96
+ }}
97
+ >
98
+ <Text
99
+ style={{
100
+ color,
101
+ fontSize: 10,
102
+ fontWeight: "700",
103
+ letterSpacing: 1,
104
+ }}
105
+ >
106
+ {label}
107
+ </Text>
108
+ <Text
109
+ style={{
110
+ color: t.text,
111
+ fontSize: 20,
112
+ fontWeight: "600",
113
+ fontVariant: ["tabular-nums"],
114
+ marginTop: 2,
115
+ }}
116
+ >
117
+ {val}
118
+ </Text>
119
+ </View>
120
+ ))}
121
+ </View>
122
+
123
+ {/* HSB */}
124
+ <View style={{ flexDirection: "row", gap: 8 }}>
125
+ {[
126
+ { label: "H", value: `${hue}°` },
127
+ { label: "S", value: `${sat}%` },
128
+ { label: "B", value: `${bright}%` },
129
+ ].map(({ label, value: val }) => (
130
+ <View
131
+ key={label}
132
+ style={{
133
+ flex: 1,
134
+ backgroundColor: t.surface,
135
+ borderRadius: 10,
136
+ paddingVertical: 10,
137
+ alignItems: "center",
138
+ }}
139
+ >
140
+ <Text
141
+ style={{
142
+ color: t.textMuted,
143
+ fontSize: 10,
144
+ fontWeight: "700",
145
+ letterSpacing: 1,
146
+ }}
147
+ >
148
+ {label}
149
+ </Text>
150
+ <Text
151
+ style={{
152
+ color: t.text,
153
+ fontSize: 20,
154
+ fontWeight: "600",
155
+ fontVariant: ["tabular-nums"],
156
+ marginTop: 2,
157
+ }}
158
+ >
159
+ {val}
160
+ </Text>
161
+ </View>
162
+ ))}
163
+ </View>
164
+ </View>
165
+ );
166
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,2 @@
1
+ export { ColorPicker, type ColorPickerProps } from "./components/color-picker";
2
+ export type { ColorPickerRef, ColorPickerLabels } from "./types/misc";
@@ -0,0 +1,43 @@
1
+ import type { ColorPickerLabels } from "../types/misc";
2
+
3
+ export const DEFAULT_LABELS: Required<ColorPickerLabels> = {
4
+ picker: "Picker",
5
+ values: "Values",
6
+ recent: "Recent",
7
+ save: "Save",
8
+ savedColors: "Saved Colors",
9
+ clearAll: "Clear All",
10
+ noSavedColors: "No saved colors yet",
11
+ noSavedColorsHint: 'Tap "Save" to add colors here',
12
+ };
13
+
14
+ export const themes = {
15
+ dark: {
16
+ background: "#1A1A2E",
17
+ surface: "rgba(255,255,255,0.06)",
18
+ border: "rgba(255,255,255,0.08)",
19
+ text: "#FFFFFF",
20
+ textMuted: "rgba(255,255,255,0.4)",
21
+ textDim: "rgba(255,255,255,0.2)",
22
+ inputBg: "rgba(255,255,255,0.08)",
23
+ thumbBorder: "#FFFFFF",
24
+ overlay: "rgba(0,0,0,0.6)",
25
+ tabActive: "#FFFFFF",
26
+ tabInactive: "rgba(255,255,255,0.3)",
27
+ tabIndicator: "#FFFFFF",
28
+ },
29
+ light: {
30
+ background: "#FFFFFF",
31
+ surface: "rgba(0,0,0,0.04)",
32
+ border: "rgba(0,0,0,0.08)",
33
+ text: "#000000",
34
+ textMuted: "rgba(0,0,0,0.5)",
35
+ textDim: "rgba(0,0,0,0.2)",
36
+ inputBg: "rgba(0,0,0,0.05)",
37
+ thumbBorder: "#FFFFFF",
38
+ overlay: "rgba(0,0,0,0.4)",
39
+ tabActive: "#000000",
40
+ tabInactive: "rgba(0,0,0,0.3)",
41
+ tabIndicator: "#000000",
42
+ },
43
+ };
@@ -0,0 +1,24 @@
1
+ import type { themes } from "../shared/const";
2
+
3
+ export type ColorPickerRef = {
4
+ getColor: () => string;
5
+ setColor: (hex: string) => void;
6
+ clearRecent: () => void;
7
+ open: () => void;
8
+ close: () => void;
9
+ };
10
+
11
+ export type TabId = "picker" | "values" | "recent";
12
+
13
+ export type ColorPickerLabels = {
14
+ picker?: string;
15
+ values?: string;
16
+ recent?: string;
17
+ save?: string;
18
+ savedColors?: string;
19
+ clearAll?: string;
20
+ noSavedColors?: string;
21
+ noSavedColorsHint?: string;
22
+ };
23
+
24
+ export type Theme = (typeof themes)["dark"];
@@ -0,0 +1,57 @@
1
+ export function hsbToHex(h: number, s: number, b: number): string {
2
+ const s1 = s / 100;
3
+ const b1 = b / 100;
4
+ const k = (n: number) => (n + h / 60) % 6;
5
+ const f = (n: number) =>
6
+ b1 * (1 - s1 * Math.max(0, Math.min(k(n), 4 - k(n), 1)));
7
+ const toHex = (v: number) =>
8
+ Math.round(v * 255)
9
+ .toString(16)
10
+ .padStart(2, "0");
11
+ return `#${toHex(f(5))}${toHex(f(3))}${toHex(f(1))}`;
12
+ }
13
+
14
+ export function hexToHsb(hex: string): { h: number; s: number; b: number } {
15
+ const clean = hex.replace("#", "");
16
+ const full =
17
+ clean.length === 3
18
+ ? clean
19
+ .split("")
20
+ .map((c) => c + c)
21
+ .join("")
22
+ : clean;
23
+ const r = parseInt(full.slice(0, 2), 16) / 255;
24
+ const g = parseInt(full.slice(2, 4), 16) / 255;
25
+ const b = parseInt(full.slice(4, 6), 16) / 255;
26
+ const max = Math.max(r, g, b);
27
+ const min = Math.min(r, g, b);
28
+ const d = max - min;
29
+ let h = 0;
30
+ if (d !== 0) {
31
+ if (max === r) h = ((g - b) / d + 6) % 6;
32
+ else if (max === g) h = (b - r) / d + 2;
33
+ else h = (r - g) / d + 4;
34
+ h *= 60;
35
+ }
36
+ const s = max === 0 ? 0 : (d / max) * 100;
37
+ return { h: Math.round(h), s: Math.round(s), b: Math.round(max * 100) };
38
+ }
39
+
40
+ export function hexToRgb(hex: string): { r: number; g: number; b: number } {
41
+ const clean = hex.replace("#", "");
42
+ return {
43
+ r: parseInt(clean.slice(0, 2), 16),
44
+ g: parseInt(clean.slice(2, 4), 16),
45
+ b: parseInt(clean.slice(4, 6), 16),
46
+ };
47
+ }
48
+
49
+ export function isValidHex(hex: string): boolean {
50
+ return /^#?[0-9A-Fa-f]{6}$/.test(hex);
51
+ }
52
+
53
+ export function getContrastColor(hex: string): string {
54
+ const { r, g, b } = hexToRgb(hex);
55
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
56
+ return luminance > 0.5 ? "#000000" : "#FFFFFF";
57
+ }