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.
Files changed (49) hide show
  1. package/README.md +7 -2
  2. package/lib/commonjs/components/CustomPages.js.map +1 -1
  3. package/lib/commonjs/components/OnboardingPages.js +45 -11
  4. package/lib/commonjs/components/OnboardingPages.js.map +1 -1
  5. package/lib/commonjs/components/Page.js.map +1 -1
  6. package/lib/commonjs/components/Pagination.js +4 -2
  7. package/lib/commonjs/components/Pagination.js.map +1 -1
  8. package/lib/commonjs/components/Swiper.js +18 -1
  9. package/lib/commonjs/components/Swiper.js.map +1 -1
  10. package/lib/commonjs/components/button.js +10 -3
  11. package/lib/commonjs/components/button.js.map +1 -1
  12. package/lib/commonjs/utils/color.js +308 -0
  13. package/lib/commonjs/utils/color.js.map +1 -0
  14. package/lib/module/components/CustomPages.js.map +1 -1
  15. package/lib/module/components/OnboardingPages.js +46 -11
  16. package/lib/module/components/OnboardingPages.js.map +1 -1
  17. package/lib/module/components/Page.js.map +1 -1
  18. package/lib/module/components/Pagination.js +4 -2
  19. package/lib/module/components/Pagination.js.map +1 -1
  20. package/lib/module/components/Swiper.js +18 -1
  21. package/lib/module/components/Swiper.js.map +1 -1
  22. package/lib/module/components/button.js +10 -3
  23. package/lib/module/components/button.js.map +1 -1
  24. package/lib/module/utils/color.js +299 -0
  25. package/lib/module/utils/color.js.map +1 -0
  26. package/lib/typescript/src/components/CustomPages.d.ts +1 -0
  27. package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
  28. package/lib/typescript/src/components/OnboardingPages.d.ts +1 -0
  29. package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
  30. package/lib/typescript/src/components/Page.d.ts +16 -0
  31. package/lib/typescript/src/components/Page.d.ts.map +1 -1
  32. package/lib/typescript/src/components/Pagination.d.ts +2 -0
  33. package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
  34. package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
  35. package/lib/typescript/src/components/button.d.ts +1 -0
  36. package/lib/typescript/src/components/button.d.ts.map +1 -1
  37. package/lib/typescript/src/types/index.d.ts +5 -0
  38. package/lib/typescript/src/types/index.d.ts.map +1 -1
  39. package/lib/typescript/src/utils/color.d.ts +18 -0
  40. package/lib/typescript/src/utils/color.d.ts.map +1 -0
  41. package/package.json +1 -5
  42. package/src/components/CustomPages.tsx +1 -0
  43. package/src/components/OnboardingPages.tsx +68 -12
  44. package/src/components/Page.tsx +16 -0
  45. package/src/components/Pagination.tsx +6 -6
  46. package/src/components/Swiper.tsx +14 -0
  47. package/src/components/button.tsx +8 -2
  48. package/src/types/index.ts +5 -0
  49. package/src/utils/color.ts +284 -0
@@ -1,7 +1,8 @@
1
1
  import React, { useMemo } from 'react';
2
- import tinycolor from 'tinycolor2';
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 = tinycolor(currentBackgroundColor).getBrightness() > 180;
41
+ const isLight = getBrightness(currentBackgroundColor) > 180;
40
42
  const footerBackgroundColor = isLight
41
- ? tinycolor(currentBackgroundColor).darken(30).toString()
42
- : tinycolor(currentBackgroundColor).lighten(30).toString();
43
+ ? darken(currentBackgroundColor, 30)
44
+ : lighten(currentBackgroundColor, 30);
43
45
  const color =
44
- tinycolor(footerBackgroundColor).getBrightness() > 180
45
- ? tinycolor(footerBackgroundColor).darken(60).toString()
46
- : tinycolor(footerBackgroundColor).lighten(60).toString();
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: props.skipLabel,
76
- nextLabel: props.nextLabel,
93
+ skipLabel,
94
+ nextLabel,
77
95
  previousLabel: props.previousLabel,
78
96
  hasSkipPosition: !!props.skipButtonPosition,
79
- doneLabel: props.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={props.skipLabel}
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={props.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
  />
@@ -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 tinycolor from 'tinycolor2';
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
- style={props.buttonStyle}
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
  });
@@ -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
+ }