react-native-app-onboard 0.2.0 → 0.3.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 +7 -2
- package/lib/commonjs/components/CustomPages.js.map +1 -1
- package/lib/commonjs/components/OnboardingPages.js +45 -11
- package/lib/commonjs/components/OnboardingPages.js.map +1 -1
- package/lib/commonjs/components/Page.js.map +1 -1
- package/lib/commonjs/components/Pagination.js +4 -2
- package/lib/commonjs/components/Pagination.js.map +1 -1
- package/lib/commonjs/components/Swiper.js +18 -1
- package/lib/commonjs/components/Swiper.js.map +1 -1
- package/lib/commonjs/components/button.js +10 -3
- package/lib/commonjs/components/button.js.map +1 -1
- package/lib/commonjs/utils/color.js +308 -0
- package/lib/commonjs/utils/color.js.map +1 -0
- package/lib/module/components/CustomPages.js.map +1 -1
- package/lib/module/components/OnboardingPages.js +46 -11
- package/lib/module/components/OnboardingPages.js.map +1 -1
- package/lib/module/components/Page.js.map +1 -1
- package/lib/module/components/Pagination.js +4 -2
- package/lib/module/components/Pagination.js.map +1 -1
- package/lib/module/components/Swiper.js +18 -1
- package/lib/module/components/Swiper.js.map +1 -1
- package/lib/module/components/button.js +10 -3
- package/lib/module/components/button.js.map +1 -1
- package/lib/module/utils/color.js +299 -0
- package/lib/module/utils/color.js.map +1 -0
- package/lib/typescript/src/components/CustomPages.d.ts +1 -0
- package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
- package/lib/typescript/src/components/OnboardingPages.d.ts +1 -0
- package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
- package/lib/typescript/src/components/Page.d.ts +16 -0
- package/lib/typescript/src/components/Page.d.ts.map +1 -1
- package/lib/typescript/src/components/Pagination.d.ts +2 -0
- package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
- package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
- package/lib/typescript/src/components/button.d.ts +1 -0
- package/lib/typescript/src/components/button.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +5 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/color.d.ts +18 -0
- package/lib/typescript/src/utils/color.d.ts.map +1 -0
- package/package.json +1 -5
- package/src/components/CustomPages.tsx +1 -0
- package/src/components/OnboardingPages.tsx +68 -12
- package/src/components/Page.tsx +16 -0
- package/src/components/Pagination.tsx +6 -6
- package/src/components/Swiper.tsx +14 -0
- package/src/components/button.tsx +8 -2
- package/src/types/index.ts +5 -0
- package/src/utils/color.ts +284 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import
|
|
2
|
+
import { getBrightness, lighten, darken } from '../utils/color';
|
|
3
3
|
import {
|
|
4
4
|
Animated,
|
|
5
|
+
View,
|
|
5
6
|
StyleSheet,
|
|
6
7
|
Dimensions,
|
|
7
8
|
FlatList,
|
|
@@ -25,6 +26,7 @@ type Props = OnboardingProps & {
|
|
|
25
26
|
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
26
27
|
onScrollBeginDrag: () => void;
|
|
27
28
|
nextPage: () => void;
|
|
29
|
+
scrollTo: (index: number, animated?: boolean) => void;
|
|
28
30
|
mirror?: boolean;
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -36,14 +38,30 @@ export const OnboardingPages = ({
|
|
|
36
38
|
const pageWidth = props.width || width;
|
|
37
39
|
const currentPage_ = props.pages[props.currentPage];
|
|
38
40
|
const currentBackgroundColor = currentPage_?.backgroundColor ?? 'white';
|
|
39
|
-
const isLight =
|
|
41
|
+
const isLight = getBrightness(currentBackgroundColor) > 180;
|
|
40
42
|
const footerBackgroundColor = isLight
|
|
41
|
-
?
|
|
42
|
-
:
|
|
43
|
+
? darken(currentBackgroundColor, 30)
|
|
44
|
+
: lighten(currentBackgroundColor, 30);
|
|
43
45
|
const color =
|
|
44
|
-
|
|
45
|
-
?
|
|
46
|
-
:
|
|
46
|
+
getBrightness(footerBackgroundColor) > 180
|
|
47
|
+
? darken(footerBackgroundColor, 60)
|
|
48
|
+
: lighten(footerBackgroundColor, 60);
|
|
49
|
+
|
|
50
|
+
// Per-page label overrides fall back to the top-level labels.
|
|
51
|
+
const nextLabel = currentPage_?.nextLabel ?? props.nextLabel;
|
|
52
|
+
const skipLabel = currentPage_?.skipLabel ?? props.skipLabel;
|
|
53
|
+
const doneLabel = currentPage_?.doneLabel ?? props.doneLabel;
|
|
54
|
+
|
|
55
|
+
// Per-page navigation gating (default allowed).
|
|
56
|
+
const canGoForward = currentPage_?.canSwipeForward !== false;
|
|
57
|
+
const canGoBackward = currentPage_?.canSwipeBackward !== false;
|
|
58
|
+
// Disable the gesture entirely when the current page gates either direction
|
|
59
|
+
// (FlatList can't block a single direction), so there's no swipe-then-snap
|
|
60
|
+
// bounce. The Next/Back buttons still navigate programmatically. Also honors
|
|
61
|
+
// a consumer-level `scrollEnabled={false}`.
|
|
62
|
+
const swipeEnabled =
|
|
63
|
+
props.scrollEnabled !== false && canGoForward && canGoBackward;
|
|
64
|
+
const hasCustomBackground = props.pages.some((p) => p.background != null);
|
|
47
65
|
|
|
48
66
|
const interpolatedBackgroundColor = useMemo(() => {
|
|
49
67
|
const pages = props.pages;
|
|
@@ -72,11 +90,13 @@ export const OnboardingPages = ({
|
|
|
72
90
|
animatedValue: props.dotsAnimatedValue,
|
|
73
91
|
showSkip: props.showSkip,
|
|
74
92
|
numberOfScreens: props.pages.length,
|
|
75
|
-
skipLabel
|
|
76
|
-
nextLabel
|
|
93
|
+
skipLabel,
|
|
94
|
+
nextLabel,
|
|
77
95
|
previousLabel: props.previousLabel,
|
|
78
96
|
hasSkipPosition: !!props.skipButtonPosition,
|
|
79
|
-
doneLabel
|
|
97
|
+
doneLabel,
|
|
98
|
+
nextDisabled: !canGoForward,
|
|
99
|
+
previousDisabled: !canGoBackward,
|
|
80
100
|
paginationStyle: props.paginationStyle,
|
|
81
101
|
progressBarStyle: props.progressBarStyle,
|
|
82
102
|
progressBarFillStyle: props.progressBarFillStyle,
|
|
@@ -103,12 +123,39 @@ export const OnboardingPages = ({
|
|
|
103
123
|
{ backgroundColor: interpolatedBackgroundColor },
|
|
104
124
|
]}
|
|
105
125
|
>
|
|
126
|
+
{hasCustomBackground && (
|
|
127
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
128
|
+
{props.pages.map((page, index) =>
|
|
129
|
+
page.background == null ? null : (
|
|
130
|
+
<Animated.View
|
|
131
|
+
key={index}
|
|
132
|
+
style={[
|
|
133
|
+
StyleSheet.absoluteFill,
|
|
134
|
+
{
|
|
135
|
+
opacity: props.scrollX.interpolate({
|
|
136
|
+
inputRange: [
|
|
137
|
+
(index - 1) * pageWidth,
|
|
138
|
+
index * pageWidth,
|
|
139
|
+
(index + 1) * pageWidth,
|
|
140
|
+
],
|
|
141
|
+
outputRange: [0, 1, 0],
|
|
142
|
+
extrapolate: 'clamp',
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
]}
|
|
146
|
+
>
|
|
147
|
+
{page.background}
|
|
148
|
+
</Animated.View>
|
|
149
|
+
)
|
|
150
|
+
)}
|
|
151
|
+
</View>
|
|
152
|
+
)}
|
|
106
153
|
{props.skipButtonPosition && props.showSkip && (
|
|
107
154
|
<SkipButton
|
|
108
155
|
buttonTextStyle={props.skipLabelStyle}
|
|
109
156
|
buttonStyle={props.skipButtonContainerStyle}
|
|
110
157
|
position={props.skipButtonPosition}
|
|
111
|
-
label={
|
|
158
|
+
label={skipLabel}
|
|
112
159
|
onPress={props.onSkip}
|
|
113
160
|
/>
|
|
114
161
|
)}
|
|
@@ -128,7 +175,7 @@ export const OnboardingPages = ({
|
|
|
128
175
|
horizontal
|
|
129
176
|
pagingEnabled
|
|
130
177
|
showsHorizontalScrollIndicator={false}
|
|
131
|
-
scrollEnabled={
|
|
178
|
+
scrollEnabled={swipeEnabled}
|
|
132
179
|
style={props.mirror ? styles.mirror : undefined}
|
|
133
180
|
keyExtractor={(_, index) => index.toString()}
|
|
134
181
|
renderItem={({ item, index }) => (
|
|
@@ -148,6 +195,15 @@ export const OnboardingPages = ({
|
|
|
148
195
|
const pageIndex = Math.round(
|
|
149
196
|
event.nativeEvent.contentOffset.x / pageWidth
|
|
150
197
|
);
|
|
198
|
+
const current = props.currentPage;
|
|
199
|
+
// Honor per-page swipe gating by snapping back to the current page.
|
|
200
|
+
if (
|
|
201
|
+
(pageIndex > current && !canGoForward) ||
|
|
202
|
+
(pageIndex < current && !canGoBackward)
|
|
203
|
+
) {
|
|
204
|
+
props.scrollTo(current);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
151
207
|
props.setPage(pageIndex || 0);
|
|
152
208
|
}}
|
|
153
209
|
/>
|
package/src/components/Page.tsx
CHANGED
|
@@ -14,6 +14,12 @@ export type Page = {
|
|
|
14
14
|
subtitle: string;
|
|
15
15
|
image: React.ReactNode;
|
|
16
16
|
backgroundColor: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional custom background element (e.g. a `LinearGradient`) rendered behind
|
|
19
|
+
* the page content and cross-faded as the user swipes. Falls back to
|
|
20
|
+
* `backgroundColor` when omitted.
|
|
21
|
+
*/
|
|
22
|
+
background?: React.ReactNode;
|
|
17
23
|
color?: string;
|
|
18
24
|
width?: number;
|
|
19
25
|
containerStyle?: StyleProp<ViewStyle>;
|
|
@@ -22,6 +28,16 @@ export type Page = {
|
|
|
22
28
|
titleStyle?: StyleProp<TextStyle>;
|
|
23
29
|
subtitleStyle?: StyleProp<TextStyle>;
|
|
24
30
|
swap?: boolean;
|
|
31
|
+
/** Per-page override for the "Next" button label. */
|
|
32
|
+
nextLabel?: string | React.ReactNode;
|
|
33
|
+
/** Per-page override for the "Skip" button label. */
|
|
34
|
+
skipLabel?: string | React.ReactNode;
|
|
35
|
+
/** Per-page override for the "Done" button label. */
|
|
36
|
+
doneLabel?: string | React.ReactNode;
|
|
37
|
+
/** When `false`, blocks advancing past this page (swipe snaps back, Next disabled). */
|
|
38
|
+
canSwipeForward?: boolean;
|
|
39
|
+
/** When `false`, blocks returning from this page (swipe snaps back). */
|
|
40
|
+
canSwipeBackward?: boolean;
|
|
25
41
|
/** Internal: counter-flips page content when the slider is mirrored for RTL. */
|
|
26
42
|
mirror?: boolean;
|
|
27
43
|
};
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type TextStyle,
|
|
9
9
|
} from 'react-native';
|
|
10
10
|
import React from 'react';
|
|
11
|
-
import
|
|
11
|
+
import { setAlpha } from '../utils/color';
|
|
12
12
|
import { useOnboarding } from '../hooks/useOnboarding';
|
|
13
13
|
import { Button } from './button';
|
|
14
14
|
|
|
@@ -45,6 +45,8 @@ type FooterProps = {
|
|
|
45
45
|
progressBarFillStyle?: StyleProp<ViewStyle>;
|
|
46
46
|
dotsAreTappable?: boolean;
|
|
47
47
|
mirror?: boolean;
|
|
48
|
+
nextDisabled?: boolean;
|
|
49
|
+
previousDisabled?: boolean;
|
|
48
50
|
onDone?: () => void;
|
|
49
51
|
onSkip?: () => void;
|
|
50
52
|
onNext?: () => void;
|
|
@@ -77,6 +79,7 @@ export function Pagination(props: FooterProps) {
|
|
|
77
79
|
{showPrevious && (
|
|
78
80
|
<Button
|
|
79
81
|
onPress={() => previousPage()}
|
|
82
|
+
disabled={props.previousDisabled}
|
|
80
83
|
buttonTextStyle={props.previousLabelStyle}
|
|
81
84
|
buttonStyle={props.previousButtonContainerStyle}
|
|
82
85
|
label={props.previousLabel || 'Back'}
|
|
@@ -101,11 +104,7 @@ export function Pagination(props: FooterProps) {
|
|
|
101
104
|
styles.progressTrack,
|
|
102
105
|
// Derive a faint track from the (background-aware) fill color so it
|
|
103
106
|
// stays visible on both light and dark pages.
|
|
104
|
-
{
|
|
105
|
-
backgroundColor: tinycolor(props.color)
|
|
106
|
-
.setAlpha(0.25)
|
|
107
|
-
.toRgbString(),
|
|
108
|
-
},
|
|
107
|
+
{ backgroundColor: setAlpha(props.color, 0.25) },
|
|
109
108
|
// Mirror the fill direction so it grows from the trailing edge.
|
|
110
109
|
props.mirror && styles.mirror,
|
|
111
110
|
props.progressBarStyle,
|
|
@@ -181,6 +180,7 @@ export function Pagination(props: FooterProps) {
|
|
|
181
180
|
{!isDone && props.showNext && (
|
|
182
181
|
<Button
|
|
183
182
|
onPress={props.onNext}
|
|
183
|
+
disabled={props.nextDisabled}
|
|
184
184
|
label={props.nextLabel || 'Next'}
|
|
185
185
|
buttonTextStyle={props.nextLabelStyle}
|
|
186
186
|
buttonStyle={props.nextButtonContainerStyle}
|
|
@@ -34,10 +34,22 @@ export const Swiper: React.FC<OnboardingProps> = (props) => {
|
|
|
34
34
|
currentPage,
|
|
35
35
|
numberOfScreens,
|
|
36
36
|
nextPage,
|
|
37
|
+
scrollTo,
|
|
37
38
|
scrollEnabled,
|
|
38
39
|
pauseAutoPlay,
|
|
39
40
|
} = useOnboarding();
|
|
40
41
|
|
|
42
|
+
// When `skipToPage` is set, "Skip" navigates within the flow instead of
|
|
43
|
+
// exiting it (so `onSkip` is not called in that case).
|
|
44
|
+
const { skipToPage, onSkip } = props;
|
|
45
|
+
const handleSkip = React.useCallback(() => {
|
|
46
|
+
if (skipToPage != null) {
|
|
47
|
+
scrollTo(skipToPage);
|
|
48
|
+
} else {
|
|
49
|
+
onSkip?.();
|
|
50
|
+
}
|
|
51
|
+
}, [skipToPage, scrollTo, onSkip]);
|
|
52
|
+
|
|
41
53
|
const onScroll = React.useMemo(
|
|
42
54
|
() =>
|
|
43
55
|
nativeDriverEnabled
|
|
@@ -71,8 +83,10 @@ export const Swiper: React.FC<OnboardingProps> = (props) => {
|
|
|
71
83
|
currentPage,
|
|
72
84
|
numberOfScreens,
|
|
73
85
|
nextPage,
|
|
86
|
+
scrollTo,
|
|
74
87
|
scrollEnabled,
|
|
75
88
|
mirror,
|
|
89
|
+
onSkip: handleSkip,
|
|
76
90
|
};
|
|
77
91
|
|
|
78
92
|
if (props.children) {
|
|
@@ -12,6 +12,7 @@ type ButtonProps = {
|
|
|
12
12
|
onPress?: () => void;
|
|
13
13
|
label?: string | React.ReactNode;
|
|
14
14
|
color?: string;
|
|
15
|
+
disabled?: boolean;
|
|
15
16
|
buttonStyle?: StyleProp<ViewStyle>;
|
|
16
17
|
buttonTextStyle?: StyleProp<TextStyle>;
|
|
17
18
|
};
|
|
@@ -19,10 +20,12 @@ type ButtonProps = {
|
|
|
19
20
|
export const Button = (props: ButtonProps) => {
|
|
20
21
|
return typeof props.label === 'string' ? (
|
|
21
22
|
<TouchableOpacity
|
|
22
|
-
onPress={props.onPress}
|
|
23
|
-
|
|
23
|
+
onPress={props.disabled ? undefined : props.onPress}
|
|
24
|
+
disabled={props.disabled}
|
|
25
|
+
style={[props.buttonStyle, props.disabled && styles.disabled]}
|
|
24
26
|
accessibilityRole="button"
|
|
25
27
|
accessibilityLabel={props.label}
|
|
28
|
+
accessibilityState={{ disabled: !!props.disabled }}
|
|
26
29
|
>
|
|
27
30
|
<Text
|
|
28
31
|
style={[
|
|
@@ -102,4 +105,7 @@ const styles = StyleSheet.create({
|
|
|
102
105
|
position: 'absolute',
|
|
103
106
|
zIndex: 10,
|
|
104
107
|
},
|
|
108
|
+
disabled: {
|
|
109
|
+
opacity: 0.4,
|
|
110
|
+
},
|
|
105
111
|
});
|
package/src/types/index.ts
CHANGED
|
@@ -14,6 +14,11 @@ export type OnboardingProps = {
|
|
|
14
14
|
onDone?: () => void;
|
|
15
15
|
onSkip?: () => void;
|
|
16
16
|
onPageChange?: (index: number) => void;
|
|
17
|
+
/**
|
|
18
|
+
* When set, pressing "Skip" navigates to this page index instead of firing
|
|
19
|
+
* `onSkip` (e.g. skip intro slides but land on a sign-up slide in the flow).
|
|
20
|
+
*/
|
|
21
|
+
skipToPage?: number;
|
|
17
22
|
showPagination?: boolean;
|
|
18
23
|
scrollEnabled?: boolean;
|
|
19
24
|
autoPlay?: boolean;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal color helpers — a tiny, dependency-free replacement for the subset of
|
|
3
|
+
* tinycolor2 this library used: perceived brightness, HSL-based lighten/darken,
|
|
4
|
+
* and producing an `rgba()` string with a custom alpha.
|
|
5
|
+
*
|
|
6
|
+
* Accepts hex (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`), `rgb()`/`rgba()`, and
|
|
7
|
+
* CSS named colors. Invalid input resolves to black, matching tinycolor's
|
|
8
|
+
* behavior of treating unparseable colors as having zero brightness.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type RGBA = { r: number; g: number; b: number; a: number };
|
|
12
|
+
|
|
13
|
+
// CSS extended color keywords (same set tinycolor recognizes).
|
|
14
|
+
const NAMED_COLORS: Record<string, string> = {
|
|
15
|
+
aliceblue: '#f0f8ff',
|
|
16
|
+
antiquewhite: '#faebd7',
|
|
17
|
+
aqua: '#00ffff',
|
|
18
|
+
aquamarine: '#7fffd4',
|
|
19
|
+
azure: '#f0ffff',
|
|
20
|
+
beige: '#f5f5dc',
|
|
21
|
+
bisque: '#ffe4c4',
|
|
22
|
+
black: '#000000',
|
|
23
|
+
blanchedalmond: '#ffebcd',
|
|
24
|
+
blue: '#0000ff',
|
|
25
|
+
blueviolet: '#8a2be2',
|
|
26
|
+
brown: '#a52a2a',
|
|
27
|
+
burlywood: '#deb887',
|
|
28
|
+
cadetblue: '#5f9ea0',
|
|
29
|
+
chartreuse: '#7fff00',
|
|
30
|
+
chocolate: '#d2691e',
|
|
31
|
+
coral: '#ff7f50',
|
|
32
|
+
cornflowerblue: '#6495ed',
|
|
33
|
+
cornsilk: '#fff8dc',
|
|
34
|
+
crimson: '#dc143c',
|
|
35
|
+
cyan: '#00ffff',
|
|
36
|
+
darkblue: '#00008b',
|
|
37
|
+
darkcyan: '#008b8b',
|
|
38
|
+
darkgoldenrod: '#b8860b',
|
|
39
|
+
darkgray: '#a9a9a9',
|
|
40
|
+
darkgreen: '#006400',
|
|
41
|
+
darkgrey: '#a9a9a9',
|
|
42
|
+
darkkhaki: '#bdb76b',
|
|
43
|
+
darkmagenta: '#8b008b',
|
|
44
|
+
darkolivegreen: '#556b2f',
|
|
45
|
+
darkorange: '#ff8c00',
|
|
46
|
+
darkorchid: '#9932cc',
|
|
47
|
+
darkred: '#8b0000',
|
|
48
|
+
darksalmon: '#e9967a',
|
|
49
|
+
darkseagreen: '#8fbc8f',
|
|
50
|
+
darkslateblue: '#483d8b',
|
|
51
|
+
darkslategray: '#2f4f4f',
|
|
52
|
+
darkslategrey: '#2f4f4f',
|
|
53
|
+
darkturquoise: '#00ced1',
|
|
54
|
+
darkviolet: '#9400d3',
|
|
55
|
+
deeppink: '#ff1493',
|
|
56
|
+
deepskyblue: '#00bfff',
|
|
57
|
+
dimgray: '#696969',
|
|
58
|
+
dimgrey: '#696969',
|
|
59
|
+
dodgerblue: '#1e90ff',
|
|
60
|
+
firebrick: '#b22222',
|
|
61
|
+
floralwhite: '#fffaf0',
|
|
62
|
+
forestgreen: '#228b22',
|
|
63
|
+
fuchsia: '#ff00ff',
|
|
64
|
+
gainsboro: '#dcdcdc',
|
|
65
|
+
ghostwhite: '#f8f8ff',
|
|
66
|
+
gold: '#ffd700',
|
|
67
|
+
goldenrod: '#daa520',
|
|
68
|
+
gray: '#808080',
|
|
69
|
+
green: '#008000',
|
|
70
|
+
greenyellow: '#adff2f',
|
|
71
|
+
grey: '#808080',
|
|
72
|
+
honeydew: '#f0fff0',
|
|
73
|
+
hotpink: '#ff69b4',
|
|
74
|
+
indianred: '#cd5c5c',
|
|
75
|
+
indigo: '#4b0082',
|
|
76
|
+
ivory: '#fffff0',
|
|
77
|
+
khaki: '#f0e68c',
|
|
78
|
+
lavender: '#e6e6fa',
|
|
79
|
+
lavenderblush: '#fff0f5',
|
|
80
|
+
lawngreen: '#7cfc00',
|
|
81
|
+
lemonchiffon: '#fffacd',
|
|
82
|
+
lightblue: '#add8e6',
|
|
83
|
+
lightcoral: '#f08080',
|
|
84
|
+
lightcyan: '#e0ffff',
|
|
85
|
+
lightgoldenrodyellow: '#fafad2',
|
|
86
|
+
lightgray: '#d3d3d3',
|
|
87
|
+
lightgreen: '#90ee90',
|
|
88
|
+
lightgrey: '#d3d3d3',
|
|
89
|
+
lightpink: '#ffb6c1',
|
|
90
|
+
lightsalmon: '#ffa07a',
|
|
91
|
+
lightseagreen: '#20b2aa',
|
|
92
|
+
lightskyblue: '#87cefa',
|
|
93
|
+
lightslategray: '#778899',
|
|
94
|
+
lightslategrey: '#778899',
|
|
95
|
+
lightsteelblue: '#b0c4de',
|
|
96
|
+
lightyellow: '#ffffe0',
|
|
97
|
+
lime: '#00ff00',
|
|
98
|
+
limegreen: '#32cd32',
|
|
99
|
+
linen: '#faf0e6',
|
|
100
|
+
magenta: '#ff00ff',
|
|
101
|
+
maroon: '#800000',
|
|
102
|
+
mediumaquamarine: '#66cdaa',
|
|
103
|
+
mediumblue: '#0000cd',
|
|
104
|
+
mediumorchid: '#ba55d3',
|
|
105
|
+
mediumpurple: '#9370db',
|
|
106
|
+
mediumseagreen: '#3cb371',
|
|
107
|
+
mediumslateblue: '#7b68ee',
|
|
108
|
+
mediumspringgreen: '#00fa9a',
|
|
109
|
+
mediumturquoise: '#48d1cc',
|
|
110
|
+
mediumvioletred: '#c71585',
|
|
111
|
+
midnightblue: '#191970',
|
|
112
|
+
mintcream: '#f5fffa',
|
|
113
|
+
mistyrose: '#ffe4e1',
|
|
114
|
+
moccasin: '#ffe4b5',
|
|
115
|
+
navajowhite: '#ffdead',
|
|
116
|
+
navy: '#000080',
|
|
117
|
+
oldlace: '#fdf5e6',
|
|
118
|
+
olive: '#808000',
|
|
119
|
+
olivedrab: '#6b8e23',
|
|
120
|
+
orange: '#ffa500',
|
|
121
|
+
orangered: '#ff4500',
|
|
122
|
+
orchid: '#da70d6',
|
|
123
|
+
palegoldenrod: '#eee8aa',
|
|
124
|
+
palegreen: '#98fb98',
|
|
125
|
+
paleturquoise: '#afeeee',
|
|
126
|
+
palevioletred: '#db7093',
|
|
127
|
+
papayawhip: '#ffefd5',
|
|
128
|
+
peachpuff: '#ffdab9',
|
|
129
|
+
peru: '#cd853f',
|
|
130
|
+
pink: '#ffc0cb',
|
|
131
|
+
plum: '#dda0dd',
|
|
132
|
+
powderblue: '#b0e0e6',
|
|
133
|
+
purple: '#800080',
|
|
134
|
+
rebeccapurple: '#663399',
|
|
135
|
+
red: '#ff0000',
|
|
136
|
+
rosybrown: '#bc8f8f',
|
|
137
|
+
royalblue: '#4169e1',
|
|
138
|
+
saddlebrown: '#8b4513',
|
|
139
|
+
salmon: '#fa8072',
|
|
140
|
+
sandybrown: '#f4a460',
|
|
141
|
+
seagreen: '#2e8b57',
|
|
142
|
+
seashell: '#fff5ee',
|
|
143
|
+
sienna: '#a0522d',
|
|
144
|
+
silver: '#c0c0c0',
|
|
145
|
+
skyblue: '#87ceeb',
|
|
146
|
+
slateblue: '#6a5acd',
|
|
147
|
+
slategray: '#708090',
|
|
148
|
+
slategrey: '#708090',
|
|
149
|
+
snow: '#fffafa',
|
|
150
|
+
springgreen: '#00ff7f',
|
|
151
|
+
steelblue: '#4682b4',
|
|
152
|
+
tan: '#d2b48c',
|
|
153
|
+
teal: '#008080',
|
|
154
|
+
thistle: '#d8bfd8',
|
|
155
|
+
tomato: '#ff6347',
|
|
156
|
+
turquoise: '#40e0d0',
|
|
157
|
+
violet: '#ee82ee',
|
|
158
|
+
wheat: '#f5deb3',
|
|
159
|
+
white: '#ffffff',
|
|
160
|
+
whitesmoke: '#f5f5f5',
|
|
161
|
+
yellow: '#ffff00',
|
|
162
|
+
yellowgreen: '#9acd32',
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
166
|
+
Math.min(max, Math.max(min, value));
|
|
167
|
+
|
|
168
|
+
function parse(input: string): RGBA {
|
|
169
|
+
let color = (input || '').trim().toLowerCase();
|
|
170
|
+
const named = NAMED_COLORS[color];
|
|
171
|
+
if (named) color = named;
|
|
172
|
+
|
|
173
|
+
if (color[0] === '#') {
|
|
174
|
+
let hex = color.slice(1);
|
|
175
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
176
|
+
hex = hex
|
|
177
|
+
.split('')
|
|
178
|
+
.map((c) => c + c)
|
|
179
|
+
.join('');
|
|
180
|
+
}
|
|
181
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
182
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
183
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
184
|
+
const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
|
|
185
|
+
if (![r, g, b].some(Number.isNaN)) return { r, g, b, a };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rgb = color.match(
|
|
189
|
+
/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/
|
|
190
|
+
);
|
|
191
|
+
if (rgb) {
|
|
192
|
+
return {
|
|
193
|
+
r: Number(rgb[1]),
|
|
194
|
+
g: Number(rgb[2]),
|
|
195
|
+
b: Number(rgb[3]),
|
|
196
|
+
a: rgb[4] !== undefined ? Number(rgb[4]) : 1,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { r: 0, g: 0, b: 0, a: 1 };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function rgbToHsl({ r, g, b }: RGBA) {
|
|
204
|
+
const rn = r / 255;
|
|
205
|
+
const gn = g / 255;
|
|
206
|
+
const bn = b / 255;
|
|
207
|
+
const max = Math.max(rn, gn, bn);
|
|
208
|
+
const min = Math.min(rn, gn, bn);
|
|
209
|
+
const l = (max + min) / 2;
|
|
210
|
+
let h = 0;
|
|
211
|
+
let s = 0;
|
|
212
|
+
if (max !== min) {
|
|
213
|
+
const d = max - min;
|
|
214
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
215
|
+
switch (max) {
|
|
216
|
+
case rn:
|
|
217
|
+
h = (gn - bn) / d + (gn < bn ? 6 : 0);
|
|
218
|
+
break;
|
|
219
|
+
case gn:
|
|
220
|
+
h = (bn - rn) / d + 2;
|
|
221
|
+
break;
|
|
222
|
+
default:
|
|
223
|
+
h = (rn - gn) / d + 4;
|
|
224
|
+
}
|
|
225
|
+
h /= 6;
|
|
226
|
+
}
|
|
227
|
+
return { h, s, l };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function hslToRgb(h: number, s: number, l: number) {
|
|
231
|
+
if (s === 0) {
|
|
232
|
+
const v = l * 255;
|
|
233
|
+
return { r: v, g: v, b: v };
|
|
234
|
+
}
|
|
235
|
+
const hue2rgb = (p: number, q: number, t: number) => {
|
|
236
|
+
let tt = t;
|
|
237
|
+
if (tt < 0) tt += 1;
|
|
238
|
+
if (tt > 1) tt -= 1;
|
|
239
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
240
|
+
if (tt < 1 / 2) return q;
|
|
241
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
242
|
+
return p;
|
|
243
|
+
};
|
|
244
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
245
|
+
const p = 2 * l - q;
|
|
246
|
+
return {
|
|
247
|
+
r: hue2rgb(p, q, h + 1 / 3) * 255,
|
|
248
|
+
g: hue2rgb(p, q, h) * 255,
|
|
249
|
+
b: hue2rgb(p, q, h - 1 / 3) * 255,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const toHex = (n: number) =>
|
|
254
|
+
Math.round(clamp(n, 0, 255))
|
|
255
|
+
.toString(16)
|
|
256
|
+
.padStart(2, '0');
|
|
257
|
+
|
|
258
|
+
/** Perceived brightness on a 0–255 scale (W3C formula, as tinycolor uses). */
|
|
259
|
+
export function getBrightness(input: string): number {
|
|
260
|
+
const { r, g, b } = parse(input);
|
|
261
|
+
return (r * 299 + g * 587 + b * 114) / 1000;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function adjustLightness(input: string, delta: number): string {
|
|
265
|
+
const hsl = rgbToHsl(parse(input));
|
|
266
|
+
const { r, g, b } = hslToRgb(hsl.h, hsl.s, clamp(hsl.l + delta, 0, 1));
|
|
267
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Lighten by `amount` percent (HSL lightness), returning a hex string. */
|
|
271
|
+
export function lighten(input: string, amount = 10): string {
|
|
272
|
+
return adjustLightness(input, amount / 100);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Darken by `amount` percent (HSL lightness), returning a hex string. */
|
|
276
|
+
export function darken(input: string, amount = 10): string {
|
|
277
|
+
return adjustLightness(input, -amount / 100);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Return an `rgba()` string for `input` with the given alpha. */
|
|
281
|
+
export function setAlpha(input: string, alpha: number): string {
|
|
282
|
+
const { r, g, b } = parse(input);
|
|
283
|
+
return `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${alpha})`;
|
|
284
|
+
}
|