@rific/auto-paper 0.4.0 → 0.6.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.
@@ -0,0 +1,59 @@
1
+ import { ThemeAppearance } from '../useComputedTheme'
2
+ import type { ColorHarmony } from '../utils/getTriadicPalette'
3
+
4
+ export type { ThemeAppearance }
5
+
6
+ export type ThemeState = {
7
+ appearance: ThemeAppearance
8
+ blur: boolean
9
+ color: string
10
+ harmony: ColorHarmony
11
+ }
12
+
13
+ const defaultInitialState: ThemeState = {
14
+ appearance: 'system',
15
+ blur: true,
16
+ color: '#6750a4',
17
+ harmony: 'split-complementary'
18
+ }
19
+
20
+ // Hand-rolled slice — no @reduxjs/toolkit dependency. Action types and creator
21
+ // behavior match the previous createSlice implementation exactly, so this works
22
+ // with RTK stores, vanilla Redux, or any reducer-based state container.
23
+ type PayloadAction<P> = { payload: P; type: string }
24
+
25
+ const createAction = <P>(type: string) => {
26
+ const actionCreator = (payload: P): PayloadAction<P> => ({ payload, type })
27
+ actionCreator.type = type
28
+ actionCreator.match = (action: { type: string }): action is PayloadAction<P> => action.type === type
29
+ return actionCreator
30
+ }
31
+
32
+ const initialize = createAction<Partial<ThemeState>>('theme/initialize')
33
+ const setAppearance = createAction<ThemeAppearance>('theme/setAppearance')
34
+ const setBlur = createAction<boolean>('theme/setBlur')
35
+ const setColor = createAction<string>('theme/setColor')
36
+ const setHarmony = createAction<ColorHarmony>('theme/setHarmony')
37
+
38
+ export const themeActions = { initialize, setAppearance, setBlur, setColor, setHarmony }
39
+
40
+ const reduce = (state: ThemeState, action: { type: string }): ThemeState => {
41
+ if (initialize.match(action)) return { ...state, ...action.payload }
42
+ if (setAppearance.match(action)) return { ...state, appearance: action.payload }
43
+ if (setBlur.match(action)) return { ...state, blur: action.payload }
44
+ if (setColor.match(action)) return { ...state, color: action.payload }
45
+ if (setHarmony.match(action)) return { ...state, harmony: action.payload }
46
+ return state
47
+ }
48
+
49
+ export function createThemeReducer(initialState?: Partial<ThemeState>) {
50
+ const initial = { ...defaultInitialState, ...initialState }
51
+ return (state: ThemeState = initial, action: { type: string }): ThemeState => reduce(state, action)
52
+ }
53
+
54
+ export const themeReducer = (state: ThemeState = defaultInitialState, action: { type: string }): ThemeState => reduce(state, action)
55
+
56
+ export const selectThemeAppearance = (state: ThemeState) => state.appearance
57
+ export const selectThemeBlur = (state: ThemeState) => state.blur
58
+ export const selectThemeColor = (state: ThemeState) => state.color
59
+ export const selectThemeHarmony = (state: ThemeState) => state.harmony
@@ -0,0 +1,79 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+ import { Appearance } from 'react-native'
3
+ import { MD3DarkTheme, MD3LightTheme, MD3Theme } from 'react-native-paper'
4
+
5
+ import { getBlendedColor } from './utils/getBlendedColor'
6
+ import { getTonalColor } from './utils/getTonalColor'
7
+ import { type ColorHarmony, getTriadicPalette } from './utils/getTriadicPalette'
8
+ import { isDarkColor } from './utils/isDarkColor'
9
+
10
+ export type ThemeAppearance = 'system' | 'light' | 'dark'
11
+
12
+ export function useComputedTheme(appearance: ThemeAppearance, color: string, harmony: ColorHarmony = 'split-complementary'): MD3Theme | null {
13
+ const [theme, setTheme] = useState<MD3Theme | null>(null)
14
+
15
+ const computeTheme = useCallback(() => {
16
+ let resolved: string | null | undefined = appearance
17
+ if (resolved === 'system') resolved = Appearance.getColorScheme()
18
+ if (resolved === 'no-preference' || !resolved) resolved = 'light'
19
+ const base = resolved === 'dark' ? MD3DarkTheme : MD3LightTheme
20
+
21
+ const _theme: MD3Theme = {
22
+ ...base,
23
+ colors: {
24
+ ...base.colors,
25
+ elevation: { ...base.colors.elevation }
26
+ }
27
+ }
28
+
29
+ const palette = getTriadicPalette(color, harmony)
30
+ const surface = base.colors.surface
31
+
32
+ _theme.colors.primary = palette.primary
33
+ _theme.colors.onPrimary = getTonalColor(palette.primary, isDarkColor(palette.primary) ? 0.95 : 0.08)
34
+ _theme.colors.primaryContainer = getBlendedColor(palette.primary, surface, 0.15)
35
+ _theme.colors.onPrimaryContainer = getTonalColor(palette.primary, isDarkColor(_theme.colors.primaryContainer) ? 0.92 : 0.12)
36
+
37
+ _theme.colors.secondary = palette.secondary
38
+ _theme.colors.onSecondary = getTonalColor(palette.secondary, isDarkColor(palette.secondary) ? 0.95 : 0.08)
39
+ _theme.colors.secondaryContainer = getBlendedColor(palette.secondary, surface, 0.15)
40
+ _theme.colors.onSecondaryContainer = getTonalColor(palette.secondary, isDarkColor(_theme.colors.secondaryContainer) ? 0.92 : 0.12)
41
+
42
+ _theme.colors.tertiary = palette.tertiary
43
+ _theme.colors.onTertiary = getTonalColor(palette.tertiary, isDarkColor(palette.tertiary) ? 0.95 : 0.08)
44
+ _theme.colors.tertiaryContainer = getBlendedColor(palette.tertiary, surface, 0.15)
45
+ _theme.colors.onTertiaryContainer = getTonalColor(palette.tertiary, isDarkColor(_theme.colors.tertiaryContainer) ? 0.92 : 0.12)
46
+
47
+ const blended = getBlendedColor(color, palette.secondary, 0.5)
48
+ const tintSurface = (alpha: number) => getBlendedColor(blended, surface, alpha)
49
+
50
+ _theme.colors.background = base.dark ? '#0F0F0F' : '#F0F0F0'
51
+ _theme.colors.surface = surface
52
+ _theme.colors.surfaceVariant = tintSurface(0.2)
53
+ _theme.colors.outline = getBlendedColor(color, base.colors.outline, 0.45)
54
+
55
+ const grey = base.dark ? '#424242' : '#c2c2c2'
56
+ const blendedGrey = getBlendedColor(blended, grey, 0.05)
57
+ _theme.colors.elevation = {
58
+ ..._theme.colors.elevation,
59
+ level1: _theme.colors.surface,
60
+ level2: getBlendedColor(blendedGrey, _theme.colors.surface, 0.25),
61
+ level3: getBlendedColor(blendedGrey, _theme.colors.surface, 0.4),
62
+ level4: getBlendedColor(blendedGrey, _theme.colors.surface, 0.55),
63
+ level5: getBlendedColor(blendedGrey, _theme.colors.surface, 0.7)
64
+ }
65
+
66
+ setTheme(_theme)
67
+ }, [appearance, color, harmony])
68
+
69
+ useEffect(() => {
70
+ computeTheme()
71
+ }, [computeTheme])
72
+
73
+ useEffect(() => {
74
+ const subscription = Appearance.addChangeListener(computeTheme)
75
+ return subscription.remove
76
+ }, [computeTheme])
77
+
78
+ return theme
79
+ }
@@ -0,0 +1,5 @@
1
+ import { useContext } from 'react'
2
+
3
+ import { ThemeSettingsContext } from './ThemeSettingsContext'
4
+
5
+ export const useThemeSettings = () => useContext(ThemeSettingsContext)
@@ -0,0 +1,151 @@
1
+ export const colorNames: Record<string, string> = {
2
+ aliceblue: '#f0f8ff',
3
+ antiquewhite: '#faebd7',
4
+ aqua: '#00ffff',
5
+ aquamarine: '#7fffd4',
6
+ azure: '#f0ffff',
7
+ beige: '#f5f5dc',
8
+ bisque: '#ffe4c4',
9
+ black: '#000000',
10
+ blanchedalmond: '#ffebcd',
11
+ blue: '#0000ff',
12
+ blueviolet: '#8a2be2',
13
+ brown: '#a52a2a',
14
+ burlywood: '#deb887',
15
+ cadetblue: '#5f9ea0',
16
+ chartreuse: '#7fff00',
17
+ chocolate: '#d2691e',
18
+ coral: '#ff7f50',
19
+ cornflowerblue: '#6495ed',
20
+ cornsilk: '#fff8dc',
21
+ crimson: '#dc143c',
22
+ cyan: '#00ffff',
23
+ darkblue: '#00008b',
24
+ darkcyan: '#008b8b',
25
+ darkgoldenrod: '#b8860b',
26
+ darkgray: '#a9a9a9',
27
+ darkgrey: '#a9a9a9',
28
+ darkgreen: '#006400',
29
+ darkkhaki: '#bdb76b',
30
+ darkmagenta: '#8b008b',
31
+ darkolivegreen: '#556b2f',
32
+ darkorange: '#ff8c00',
33
+ darkorchid: '#9932cc',
34
+ darkred: '#8b0000',
35
+ darksalmon: '#e9967a',
36
+ darkseagreen: '#8fbc8f',
37
+ darkslateblue: '#483d8b',
38
+ darkslategray: '#2f4f4f',
39
+ darkslategrey: '#2f4f4f',
40
+ darkturquoise: '#00ced1',
41
+ darkviolet: '#9400d3',
42
+ deeppink: '#ff1493',
43
+ deepskyblue: '#00bfff',
44
+ dimgray: '#696969',
45
+ dimgrey: '#696969',
46
+ dodgerblue: '#1e90ff',
47
+ firebrick: '#b22222',
48
+ floralwhite: '#fffaf0',
49
+ forestgreen: '#228b22',
50
+ fuchsia: '#ff00ff',
51
+ gainsboro: '#dcdcdc',
52
+ ghostwhite: '#f8f8ff',
53
+ gold: '#ffd700',
54
+ goldenrod: '#daa520',
55
+ gray: '#808080',
56
+ grey: '#808080',
57
+ green: '#008000',
58
+ greenyellow: '#adff2f',
59
+ honeydew: '#f0fff0',
60
+ hotpink: '#ff69b4',
61
+ indianred: '#cd5c5c',
62
+ indigo: '#4b0082',
63
+ ivory: '#fffff0',
64
+ khaki: '#f0e68c',
65
+ lavender: '#e6e6fa',
66
+ lavenderblush: '#fff0f5',
67
+ lawngreen: '#7cfc00',
68
+ lemonchiffon: '#fffacd',
69
+ lightblue: '#add8e6',
70
+ lightcoral: '#f08080',
71
+ lightcyan: '#e0ffff',
72
+ lightgoldenrodyellow: '#fafad2',
73
+ lightgray: '#d3d3d3',
74
+ lightgrey: '#d3d3d3',
75
+ lightgreen: '#90ee90',
76
+ lightpink: '#ffb6c1',
77
+ lightsalmon: '#ffa07a',
78
+ lightseagreen: '#20b2aa',
79
+ lightskyblue: '#87cefa',
80
+ lightslategray: '#778899',
81
+ lightslategrey: '#778899',
82
+ lightsteelblue: '#b0c4de',
83
+ lightyellow: '#ffffe0',
84
+ lime: '#00ff00',
85
+ limegreen: '#32cd32',
86
+ linen: '#faf0e6',
87
+ magenta: '#ff00ff',
88
+ maroon: '#800000',
89
+ mediumaquamarine: '#66cdaa',
90
+ mediumblue: '#0000cd',
91
+ mediumorchid: '#ba55d3',
92
+ mediumpurple: '#9370db',
93
+ mediumseagreen: '#3cb371',
94
+ mediumslateblue: '#7b68ee',
95
+ mediumspringgreen: '#00fa9a',
96
+ mediumturquoise: '#48d1cc',
97
+ mediumvioletred: '#c71585',
98
+ midnightblue: '#191970',
99
+ mintcream: '#f5fffa',
100
+ mistyrose: '#ffe4e1',
101
+ moccasin: '#ffe4b5',
102
+ navajowhite: '#ffdead',
103
+ navy: '#000080',
104
+ oldlace: '#fdf5e6',
105
+ olive: '#808000',
106
+ olivedrab: '#6b8e23',
107
+ orange: '#ffa500',
108
+ orangered: '#ff4500',
109
+ orchid: '#da70d6',
110
+ palegoldenrod: '#eee8aa',
111
+ palegreen: '#98fb98',
112
+ paleturquoise: '#afeeee',
113
+ palevioletred: '#db7093',
114
+ papayawhip: '#ffefd5',
115
+ peachpuff: '#ffdab9',
116
+ peru: '#cd853f',
117
+ pink: '#ffc0cb',
118
+ plum: '#dda0dd',
119
+ powderblue: '#b0e0e6',
120
+ purple: '#800080',
121
+ rebeccapurple: '#663399',
122
+ red: '#ff0000',
123
+ rosybrown: '#bc8f8f',
124
+ royalblue: '#4169e1',
125
+ saddlebrown: '#8b4513',
126
+ salmon: '#fa8072',
127
+ sandybrown: '#f4a460',
128
+ seagreen: '#2e8b57',
129
+ seashell: '#fff5ee',
130
+ sienna: '#a0522d',
131
+ silver: '#c0c0c0',
132
+ skyblue: '#87ceeb',
133
+ slateblue: '#6a5acd',
134
+ slategray: '#708090',
135
+ slategrey: '#708090',
136
+ snow: '#fffafa',
137
+ springgreen: '#00ff7f',
138
+ steelblue: '#4682b4',
139
+ tan: '#d2b48c',
140
+ teal: '#008080',
141
+ thistle: '#d8bfd8',
142
+ tomato: '#ff6347',
143
+ transparent: 'transparent',
144
+ turquoise: '#40e0d0',
145
+ violet: '#ee82ee',
146
+ wheat: '#f5deb3',
147
+ white: '#ffffff',
148
+ whitesmoke: '#f5f5f5',
149
+ yellow: '#ffff00',
150
+ yellowgreen: '#9acd32'
151
+ }
@@ -0,0 +1,15 @@
1
+ import { getHex } from './getHex'
2
+ import { getRgb } from './getRgb'
3
+
4
+ export const getBlendedColor = (foreground: string, background: string, alpha: number): string => {
5
+ const fg = getRgb(foreground)
6
+ const bg = getRgb(background)
7
+ if (!fg || !bg) return background
8
+ const safeAlpha = Math.min(1, Math.max(0, alpha))
9
+ const r = Math.round(fg.r * safeAlpha + bg.r * (1 - safeAlpha))
10
+ const g = Math.round(fg.g * safeAlpha + bg.g * (1 - safeAlpha))
11
+ const b = Math.round(fg.b * safeAlpha + bg.b * (1 - safeAlpha))
12
+ return getHex(`rgb(${r}, ${g}, ${b})`) ?? background
13
+ }
14
+
15
+ export default getBlendedColor
@@ -0,0 +1,21 @@
1
+ import { getRgb } from './getRgb'
2
+
3
+ export const getHex = (value: string): string | null => {
4
+ const rgb = getRgb(value)
5
+ if (!rgb) return null
6
+
7
+ const toHex = (n: number) => {
8
+ const hex = Math.round(n).toString(16)
9
+ return hex.length === 1 ? '0' + hex : hex
10
+ }
11
+
12
+ const hexString = `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`
13
+
14
+ if (rgb.a !== undefined) {
15
+ return `${hexString}${toHex(rgb.a * 255)}`
16
+ }
17
+
18
+ return hexString
19
+ }
20
+
21
+ export default getHex
@@ -0,0 +1,65 @@
1
+ import { colorNames } from './colorNames'
2
+
3
+ export type RGB = {
4
+ r: number
5
+ g: number
6
+ b: number
7
+ a?: number
8
+ }
9
+
10
+ export const getRgb = (value: string): RGB | null => {
11
+ if (!value) return null
12
+
13
+ let trimmed = value.trim().toLowerCase()
14
+
15
+ const namedColor = colorNames[trimmed]
16
+ if (namedColor) trimmed = namedColor
17
+
18
+ const hexMatch = trimmed.match(/^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
19
+
20
+ if (hexMatch) {
21
+ const hex = hexMatch[1]
22
+ let normalized = hex
23
+
24
+ if (hex.length === 3) {
25
+ normalized = hex
26
+ .split('')
27
+ .map((c) => c + c)
28
+ .join('')
29
+ } else if (hex.length === 8) {
30
+ const alpha = parseInt(hex.substring(6, 8), 16)
31
+ normalized = hex.substring(0, 6)
32
+ const intVal = parseInt(normalized, 16)
33
+ return {
34
+ r: (intVal >> 16) & 255,
35
+ g: (intVal >> 8) & 255,
36
+ b: intVal & 255,
37
+ a: alpha / 255
38
+ }
39
+ }
40
+
41
+ const intVal = parseInt(normalized, 16)
42
+ return {
43
+ r: (intVal >> 16) & 255,
44
+ g: (intVal >> 8) & 255,
45
+ b: intVal & 255
46
+ }
47
+ }
48
+
49
+ const rgbMatch = trimmed.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([0-9.]+))?\s*\)$/i)
50
+ if (rgbMatch) {
51
+ const result: RGB = {
52
+ r: Number(rgbMatch[1]),
53
+ g: Number(rgbMatch[2]),
54
+ b: Number(rgbMatch[3])
55
+ }
56
+ if (rgbMatch[4] !== undefined) {
57
+ result.a = Number(rgbMatch[4])
58
+ }
59
+ return result
60
+ }
61
+
62
+ return null
63
+ }
64
+
65
+ export default getRgb
@@ -0,0 +1,13 @@
1
+ import { getBlendedColor } from './getBlendedColor'
2
+ import { getTonalColor } from './getTonalColor'
3
+ import { isDarkColor } from './isDarkColor'
4
+
5
+ // Computes a contrast-safe text color for content sitting on a BlurView tintColor,
6
+ // accounting for the tint being blended over the backdrop at the given opacity
7
+ // (same approach useComputedTheme uses to derive onPrimaryContainer from primaryContainer).
8
+ export const getTintTextColor = (tintColor: string, backdropColor: string, tintOpacity: number): string => {
9
+ const blended = getBlendedColor(tintColor, backdropColor, tintOpacity)
10
+ return getTonalColor(tintColor, isDarkColor(blended) ? 0.92 : 0.12)
11
+ }
12
+
13
+ export default getTintTextColor
@@ -0,0 +1,67 @@
1
+ import { getHex } from './getHex'
2
+ import { getRgb } from './getRgb'
3
+
4
+ // Returns the color with lightness clamped to the given target (0–1), preserving hue and saturation.
5
+ export const getTonalColor = (color: string, lightness: number): string => {
6
+ const rgb = getRgb(color)
7
+ if (!rgb) return color
8
+
9
+ const r = rgb.r / 255
10
+ const g = rgb.g / 255
11
+ const b = rgb.b / 255
12
+
13
+ const max = Math.max(r, g, b)
14
+ const min = Math.min(r, g, b)
15
+ const delta = max - min
16
+
17
+ let h = 0
18
+ if (delta !== 0) {
19
+ if (max === r) h = ((g - b) / delta) % 6
20
+ else if (max === g) h = (b - r) / delta + 2
21
+ else h = (r - g) / delta + 4
22
+ h = (h * 60 + 360) % 360
23
+ }
24
+
25
+ const l = (max + min) / 2
26
+ const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1))
27
+
28
+ // Rebuild with new lightness
29
+ const targetL = Math.min(1, Math.max(0, lightness))
30
+ const c = (1 - Math.abs(2 * targetL - 1)) * s
31
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1))
32
+ const m = targetL - c / 2
33
+
34
+ let r2 = 0,
35
+ g2 = 0,
36
+ b2 = 0
37
+ if (h < 60) {
38
+ r2 = c
39
+ g2 = x
40
+ b2 = 0
41
+ } else if (h < 120) {
42
+ r2 = x
43
+ g2 = c
44
+ b2 = 0
45
+ } else if (h < 180) {
46
+ r2 = 0
47
+ g2 = c
48
+ b2 = x
49
+ } else if (h < 240) {
50
+ r2 = 0
51
+ g2 = x
52
+ b2 = c
53
+ } else if (h < 300) {
54
+ r2 = x
55
+ g2 = 0
56
+ b2 = c
57
+ } else {
58
+ r2 = c
59
+ g2 = 0
60
+ b2 = x
61
+ }
62
+
63
+ const toInt = (n: number) => Math.round((n + m) * 255)
64
+ return getHex(`rgb(${toInt(r2)}, ${toInt(g2)}, ${toInt(b2)})`) ?? color
65
+ }
66
+
67
+ export default getTonalColor
@@ -0,0 +1,103 @@
1
+ import { getHex } from './getHex'
2
+ import { getRgb } from './getRgb'
3
+
4
+ export type ColorHarmony = 'triadic' | 'split-complementary' | 'analogous' | 'square' | 'complementary' | 'double-split'
5
+
6
+ export type TriadicPalette = {
7
+ primary: string
8
+ secondary: string
9
+ tertiary: string
10
+ }
11
+
12
+ const HARMONY_OFFSETS: Record<ColorHarmony, [number, number]> = {
13
+ analogous: [1 / 12, 2 / 12],
14
+ complementary: [1 / 4, 1 / 2],
15
+ 'double-split': [1 / 12, 11 / 12],
16
+ 'split-complementary': [5 / 12, 7 / 12],
17
+ square: [1 / 4, 3 / 4],
18
+ triadic: [1 / 3, 2 / 3]
19
+ }
20
+
21
+ export function getTriadicPalette(primaryColor: string, harmony: ColorHarmony = 'split-complementary'): TriadicPalette {
22
+ const rgb = getRgb(primaryColor)
23
+ if (!rgb) throw new Error(`Invalid color format: ${primaryColor}`)
24
+
25
+ const r = rgb.r / 255
26
+ const g = rgb.g / 255
27
+ const b = rgb.b / 255
28
+
29
+ const max = Math.max(r, g, b)
30
+ const min = Math.min(r, g, b)
31
+ const l = (max + min) / 2
32
+
33
+ let h = 0
34
+ let s = 0
35
+
36
+ if (max !== min) {
37
+ const d = max - min
38
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
39
+
40
+ switch (max) {
41
+ case r:
42
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6
43
+ break
44
+ case g:
45
+ h = ((b - r) / d + 2) / 6
46
+ break
47
+ case b:
48
+ h = ((r - g) / d + 4) / 6
49
+ break
50
+ }
51
+ }
52
+
53
+ const [o1, o2] = HARMONY_OFFSETS[harmony]
54
+
55
+ return {
56
+ primary: getHex(primaryColor) ?? primaryColor,
57
+ secondary: hslToHex((h + o1) % 1, s, l),
58
+ tertiary: hslToHex((h + o2) % 1, s, l)
59
+ }
60
+ }
61
+
62
+ function hslToHex(h: number, s: number, l: number): string {
63
+ const c = (1 - Math.abs(2 * l - 1)) * s
64
+ const x = c * (1 - Math.abs(((h * 6) % 2) - 1))
65
+ const m = l - c / 2
66
+
67
+ let r = 0
68
+ let g = 0
69
+ let b = 0
70
+
71
+ if (h < 1 / 6) {
72
+ r = c
73
+ g = x
74
+ b = 0
75
+ } else if (h < 2 / 6) {
76
+ r = x
77
+ g = c
78
+ b = 0
79
+ } else if (h < 3 / 6) {
80
+ r = 0
81
+ g = c
82
+ b = x
83
+ } else if (h < 4 / 6) {
84
+ r = 0
85
+ g = x
86
+ b = c
87
+ } else if (h < 5 / 6) {
88
+ r = x
89
+ g = 0
90
+ b = c
91
+ } else {
92
+ r = c
93
+ g = 0
94
+ b = x
95
+ }
96
+
97
+ const toHex = (value: number) => {
98
+ const hex = Math.round((value + m) * 255).toString(16)
99
+ return hex.length === 1 ? `0${hex}` : hex
100
+ }
101
+
102
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
103
+ }
@@ -0,0 +1,14 @@
1
+ import { getRgb } from './getRgb'
2
+
3
+ export const isDarkColor = (value: string): boolean => {
4
+ const rgb = getRgb(value)
5
+ if (!rgb) return false
6
+
7
+ const [r, g, b] = [rgb.r / 255, rgb.g / 255, rgb.b / 255]
8
+ const [rs, gs, bs] = [r, g, b].map((c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)))
9
+ const luminance = 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
10
+
11
+ return luminance < 0.5
12
+ }
13
+
14
+ export default isDarkColor