ferns-ui 0.36.3 → 0.36.5

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 (77) hide show
  1. package/package.json +3 -2
  2. package/src/ActionSheet.tsx +1231 -0
  3. package/src/Avatar.tsx +317 -0
  4. package/src/Badge.tsx +65 -0
  5. package/src/Banner.tsx +124 -0
  6. package/src/BlurBox.native.tsx +40 -0
  7. package/src/BlurBox.tsx +31 -0
  8. package/src/Body.tsx +32 -0
  9. package/src/Box.tsx +308 -0
  10. package/src/Button.tsx +219 -0
  11. package/src/Card.tsx +23 -0
  12. package/src/CheckBox.tsx +118 -0
  13. package/src/Common.ts +2743 -0
  14. package/src/Constants.ts +53 -0
  15. package/src/CustomSelect.tsx +85 -0
  16. package/src/DateTimeActionSheet.tsx +409 -0
  17. package/src/DateTimeField.android.tsx +101 -0
  18. package/src/DateTimeField.ios.tsx +83 -0
  19. package/src/DateTimeField.tsx +69 -0
  20. package/src/DecimalRangeActionSheet.tsx +113 -0
  21. package/src/ErrorBoundary.tsx +37 -0
  22. package/src/ErrorPage.tsx +44 -0
  23. package/src/FernsProvider.tsx +21 -0
  24. package/src/Field.tsx +299 -0
  25. package/src/FieldWithLabels.tsx +36 -0
  26. package/src/FlatList.tsx +2 -0
  27. package/src/Form.tsx +182 -0
  28. package/src/HeaderButtons.tsx +107 -0
  29. package/src/Heading.tsx +53 -0
  30. package/src/HeightActionSheet.tsx +104 -0
  31. package/src/Hyperlink.tsx +181 -0
  32. package/src/Icon.tsx +24 -0
  33. package/src/IconButton.tsx +165 -0
  34. package/src/Image.tsx +50 -0
  35. package/src/ImageBackground.tsx +14 -0
  36. package/src/InfoTooltipButton.tsx +23 -0
  37. package/src/Layer.tsx +17 -0
  38. package/src/Link.tsx +17 -0
  39. package/src/Mask.tsx +21 -0
  40. package/src/MediaQuery.ts +46 -0
  41. package/src/Meta.tsx +9 -0
  42. package/src/Modal.tsx +248 -0
  43. package/src/ModalSheet.tsx +58 -0
  44. package/src/NumberPickerActionSheet.tsx +66 -0
  45. package/src/Page.tsx +133 -0
  46. package/src/Permissions.ts +44 -0
  47. package/src/PickerSelect.tsx +553 -0
  48. package/src/Pill.tsx +24 -0
  49. package/src/Pog.tsx +87 -0
  50. package/src/ProgressBar.tsx +55 -0
  51. package/src/ScrollView.tsx +2 -0
  52. package/src/SegmentedControl.tsx +102 -0
  53. package/src/SelectList.tsx +89 -0
  54. package/src/SideDrawer.tsx +62 -0
  55. package/src/Spinner.tsx +20 -0
  56. package/src/SplitPage.native.tsx +160 -0
  57. package/src/SplitPage.tsx +302 -0
  58. package/src/Switch.tsx +19 -0
  59. package/src/Table.tsx +87 -0
  60. package/src/TableHeader.tsx +36 -0
  61. package/src/TableHeaderCell.tsx +76 -0
  62. package/src/TableRow.tsx +87 -0
  63. package/src/TapToEdit.tsx +221 -0
  64. package/src/Text.tsx +131 -0
  65. package/src/TextArea.tsx +16 -0
  66. package/src/TextField.tsx +401 -0
  67. package/src/TextFieldNumberActionSheet.tsx +61 -0
  68. package/src/Toast.tsx +106 -0
  69. package/src/Tooltip.tsx +269 -0
  70. package/src/UnifiedScreens.ts +24 -0
  71. package/src/Unifier.ts +371 -0
  72. package/src/Utilities.tsx +159 -0
  73. package/src/WithLabel.tsx +57 -0
  74. package/src/dayjsExtended.ts +10 -0
  75. package/src/index.tsx +1346 -0
  76. package/src/polyfill.d.ts +11 -0
  77. package/src/tableContext.tsx +80 -0
@@ -0,0 +1,269 @@
1
+ import * as React from "react";
2
+ import {
3
+ Dimensions,
4
+ LayoutChangeEvent,
5
+ LayoutRectangle,
6
+ Platform,
7
+ Pressable,
8
+ View,
9
+ } from "react-native";
10
+ import {Portal} from "react-native-portalize";
11
+
12
+ import {TooltipDirection} from "./Common";
13
+ import {Text} from "./Text";
14
+ import {Unifier} from "./Unifier";
15
+
16
+ const TOOLTIP_OFFSET = 8;
17
+ // How many pixels to leave between the tooltip and the edge of the screen
18
+ const TOOLTIP_OVERFLOW_PADDING = 20;
19
+
20
+ type ChildrenMeasurement = {
21
+ width: number;
22
+ height: number;
23
+ pageX: number;
24
+ pageY: number;
25
+ };
26
+
27
+ type Measurement = {
28
+ children: ChildrenMeasurement;
29
+ tooltip: LayoutRectangle;
30
+ measured: boolean;
31
+ idealDirection?: TooltipDirection;
32
+ };
33
+
34
+ const overflowLeft = (x: number): boolean => {
35
+ return x < TOOLTIP_OVERFLOW_PADDING;
36
+ };
37
+
38
+ const overflowRight = (x: number): boolean => {
39
+ const {width: layoutWidth} = Dimensions.get("window");
40
+ return x + TOOLTIP_OVERFLOW_PADDING > layoutWidth;
41
+ };
42
+
43
+ const getTooltipPosition = ({
44
+ children,
45
+ tooltip,
46
+ measured,
47
+ idealDirection,
48
+ }: Measurement): {} | {left: number; top: number} => {
49
+ if (!measured) {
50
+ console.debug("No measurements for child yet, cannot show tooltip yet.");
51
+ return {};
52
+ }
53
+
54
+ const {
55
+ pageY: childrenY,
56
+ height: childrenHeight,
57
+ pageX: childrenX,
58
+ width: childrenWidth,
59
+ }: ChildrenMeasurement = children;
60
+ const {width: tooltipWidth, height: tooltipHeight} = tooltip;
61
+ const horizontalCenter = childrenX + childrenWidth / 2;
62
+ const right = childrenX + childrenWidth + TOOLTIP_OFFSET;
63
+ const left = childrenX - tooltipWidth - TOOLTIP_OFFSET;
64
+
65
+ const top = childrenY - tooltipHeight - TOOLTIP_OFFSET;
66
+ const bottom = childrenY + childrenHeight + TOOLTIP_OFFSET;
67
+ const verticalCenter = top + childrenHeight + TOOLTIP_OFFSET;
68
+
69
+ // Top is overflowed if it would go off either side or the top of the screen.
70
+ const overflowTop = top < TOOLTIP_OVERFLOW_PADDING;
71
+
72
+ // Bottom is overflowed if it would go off either side or the bottom of the screen.
73
+ const overflowBottom =
74
+ bottom + tooltipHeight + TOOLTIP_OVERFLOW_PADDING > Dimensions.get("window").height;
75
+
76
+ // If it would overflow to the right, try to put it above, if not, put it on the left.
77
+ // If it would overflow to the left, try to put it above, if not, put it to the right.
78
+
79
+ // Happy path:
80
+ if (idealDirection === "left" && !overflowLeft(left)) {
81
+ return {left, top: verticalCenter};
82
+ } else if (idealDirection === "right" && !overflowRight(right + tooltipWidth)) {
83
+ return {left: right, top: verticalCenter};
84
+ } else if (
85
+ idealDirection === "bottom" &&
86
+ !overflowBottom &&
87
+ !overflowLeft(horizontalCenter - tooltipWidth) &&
88
+ !overflowRight(horizontalCenter + tooltipWidth)
89
+ ) {
90
+ return {left: horizontalCenter - tooltipWidth / 2, top: bottom};
91
+ } else {
92
+ // At this point, we're either trying to place it above or below, and force it into the viewport.
93
+
94
+ let y = top;
95
+ if ((idealDirection === "bottom" && !overflowBottom) || overflowTop) {
96
+ y = bottom;
97
+ }
98
+
99
+ // If it fits in the viewport, center it above the child.
100
+ if (
101
+ !overflowLeft(horizontalCenter - tooltipWidth) &&
102
+ !overflowRight(horizontalCenter + tooltipWidth)
103
+ ) {
104
+ return {left: horizontalCenter - tooltipWidth / 2, top: y};
105
+ }
106
+ // Failing that, if it fits on the left, put it there, otherwise to the right. We know it's smaller than the
107
+ // viewport.
108
+ else if (overflowLeft(horizontalCenter - tooltipWidth)) {
109
+ return {left: TOOLTIP_OVERFLOW_PADDING, top: y};
110
+ } else {
111
+ return {
112
+ left: Dimensions.get("window").width - TOOLTIP_OVERFLOW_PADDING - tooltipWidth,
113
+ top: y,
114
+ };
115
+ }
116
+ }
117
+ };
118
+
119
+ interface TooltipProps {
120
+ children: React.ReactElement;
121
+ // If text is undefined, the children will be rendered without a tooltip.
122
+ text?: string;
123
+ idealDirection?: "top" | "bottom" | "left" | "right";
124
+ bgColor?: "white" | "lightGray" | "gray" | "darkGray";
125
+ }
126
+
127
+ export const Tooltip = (props: TooltipProps) => {
128
+ const {text, children, bgColor, idealDirection} = props;
129
+ const hoverDelay = 500;
130
+ const hoverEndDelay = 1500;
131
+ const [visible, setVisible] = React.useState(false);
132
+
133
+ const [measurement, setMeasurement] = React.useState({
134
+ children: {},
135
+ tooltip: {},
136
+ measured: false,
137
+ });
138
+ const showTooltipTimer = React.useRef<NodeJS.Timeout>();
139
+ const hideTooltipTimer = React.useRef<NodeJS.Timeout>();
140
+ const childrenWrapperRef = React.useRef() as React.MutableRefObject<View>;
141
+
142
+ const touched = React.useRef(false);
143
+
144
+ const isWeb = Platform.OS === "web";
145
+
146
+ React.useEffect(() => {
147
+ return () => {
148
+ if (showTooltipTimer.current) {
149
+ clearTimeout(showTooltipTimer.current);
150
+ }
151
+
152
+ if (hideTooltipTimer.current) {
153
+ clearTimeout(hideTooltipTimer.current);
154
+ }
155
+ };
156
+ }, []);
157
+
158
+ const handleOnLayout = ({nativeEvent: {layout}}: LayoutChangeEvent) => {
159
+ if (childrenWrapperRef?.current && !childrenWrapperRef?.current?.measure) {
160
+ console.error("Tooltip: childrenWrapperRef does not have a measure method.");
161
+ return;
162
+ } else if (!childrenWrapperRef?.current) {
163
+ console.error("Tooltip: childrenWrapperRef is null.");
164
+ }
165
+ childrenWrapperRef?.current?.measure((_x, _y, width, height, pageX, pageY) => {
166
+ setMeasurement({
167
+ children: {pageX, pageY, height, width},
168
+ tooltip: {...layout},
169
+ measured: true,
170
+ });
171
+ });
172
+ };
173
+
174
+ const handleTouchStart = () => {
175
+ if (hideTooltipTimer.current) {
176
+ clearTimeout(hideTooltipTimer.current);
177
+ }
178
+
179
+ showTooltipTimer.current = setTimeout(() => {
180
+ touched.current = true;
181
+ setVisible(true);
182
+ }, 100);
183
+ };
184
+
185
+ const handleHoverIn = () => {
186
+ if (hideTooltipTimer.current) {
187
+ clearTimeout(hideTooltipTimer.current);
188
+ }
189
+
190
+ showTooltipTimer.current = setTimeout(() => {
191
+ touched.current = true;
192
+ setVisible(true);
193
+ }, hoverDelay);
194
+ };
195
+
196
+ const handleHoverOut = () => {
197
+ touched.current = false;
198
+ if (showTooltipTimer.current) {
199
+ clearTimeout(showTooltipTimer.current);
200
+ }
201
+
202
+ hideTooltipTimer.current = setTimeout(() => {
203
+ setVisible(false);
204
+ setMeasurement({children: {}, tooltip: {}, measured: false});
205
+ }, hoverEndDelay);
206
+ };
207
+
208
+ const mobilePressProps = {
209
+ onPress: React.useCallback(() => {
210
+ if (touched.current) {
211
+ return null;
212
+ } else {
213
+ return children.props.onClick?.();
214
+ }
215
+ }, [children.props]),
216
+ };
217
+
218
+ // Allow disabling tooltips when there is no string, otherwise you need to wrap the children in a function to
219
+ // determine if there should be a tooltip or not, which gets messy.
220
+ if (!text) {
221
+ return children;
222
+ }
223
+
224
+ return (
225
+ <>
226
+ {visible && (
227
+ <Portal>
228
+ <Pressable
229
+ style={{
230
+ alignSelf: "flex-start",
231
+ justifyContent: "center",
232
+ paddingHorizontal: 16,
233
+ backgroundColor: Unifier.theme[bgColor ?? "darkGray"],
234
+ borderRadius: 16,
235
+ paddingVertical: 8,
236
+ display: "flex",
237
+ flexShrink: 1,
238
+ maxWidth: Math.max(Dimensions.get("window").width - 32, 300),
239
+ ...getTooltipPosition({...(measurement as Measurement), idealDirection}),
240
+ ...(measurement.measured ? {opacity: 1} : {opacity: 0}),
241
+ }}
242
+ testID="tooltip-container"
243
+ onLayout={handleOnLayout}
244
+ onPress={() => {
245
+ setVisible(false);
246
+ }}
247
+ >
248
+ <Text color="white">{text}</Text>
249
+ </Pressable>
250
+ </Portal>
251
+ )}
252
+ <View
253
+ ref={childrenWrapperRef}
254
+ onPointerEnter={() => {
255
+ handleHoverIn();
256
+ children.props.onHoverIn?.();
257
+ }}
258
+ onPointerLeave={() => {
259
+ handleHoverOut();
260
+ children.props.onHoverOut?.();
261
+ }}
262
+ onTouchStart={handleTouchStart}
263
+ {...(!isWeb && mobilePressProps)}
264
+ >
265
+ {children}
266
+ </View>
267
+ </>
268
+ );
269
+ };
@@ -0,0 +1,24 @@
1
+ // import {Screens} from "./UnifiedScreens";
2
+
3
+ export enum UnifierScreens {
4
+ // BarcodeView = "BarcodeView",
5
+ Contacts = "Contacts",
6
+ FullPageModal = "FullPageModal",
7
+ Auth = "Auth",
8
+ Spinner = "lib.Spinner",
9
+ Payment = "Payment",
10
+ Onboarding = "Onboarding",
11
+ }
12
+ let initialized = false;
13
+ export function initializeUnifiedScreens() {
14
+ if (initialized) {
15
+ console.error("Cannot initialize screens multiple times.");
16
+ return;
17
+ }
18
+ initialized = true;
19
+ console.debug("[unifier] Registering unifier screens");
20
+ // Unifier.navigation.registerScreen(UnifierScreens.Auth, Auth, {url: "/auth"});
21
+ // registerScreen(Screens.BarcodeView, BarcodeView, {url: "/barcode"});
22
+ // Unifier.navigation.registerScreen(UnifierScreens.FullPageModal, FullPageModal, {url: "/item"});
23
+ // Unifier.navigation.registerScreen(UnifierScreens.Spinner, Spinner);
24
+ }
package/src/Unifier.ts ADDED
@@ -0,0 +1,371 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /* eslint-disable @typescript-eslint/no-empty-function */
3
+
4
+ import AsyncStorage from "@react-native-async-storage/async-storage";
5
+ import * as Clipboard from "expo-clipboard";
6
+ import * as Font from "expo-font";
7
+ import {FontSource} from "expo-font";
8
+ import * as Haptics from "expo-haptics";
9
+ import {Dimensions, Keyboard, Linking, Platform, Vibration} from "react-native";
10
+
11
+ import {PermissionKind, UnifiedTheme} from "./Common";
12
+ import {requestPermissions} from "./Permissions";
13
+
14
+ function capitalize(string: string) {
15
+ return string.charAt(0).toUpperCase() + string.slice(1);
16
+ }
17
+
18
+ const DEFAULT_FONT = "Cochin";
19
+ const DEFAULT_BOLD_FONT = "Cochin";
20
+
21
+ const DefaultTheme: UnifiedTheme = {
22
+ // Primary colors
23
+ red: "#bd081c",
24
+ white: "#fdfdfd",
25
+ lightGray: "#efefef",
26
+ gray: "#8e8e8e",
27
+ darkGray: "#111",
28
+ // secondary colors
29
+ green: "#0fa573",
30
+ springGreen: "#008753",
31
+ pine: "#0a6955",
32
+ olive: "#364a4c",
33
+ blue: "#4a90e2",
34
+ navy: "#004b91",
35
+ midnight: "#133a5e",
36
+ purple: "#b469eb",
37
+ orchid: "#8046a5",
38
+ eggplant: "#5b2677",
39
+ maroon: "#6e0f3c",
40
+ watermelon: "#f13535",
41
+ orange: "#e3780c",
42
+ black: "#000000",
43
+
44
+ primaryLighter: "#4ED456",
45
+ primaryLight: "#28CA32",
46
+ primary: "#00BD0C",
47
+ primaryDark: "#00960A",
48
+ primaryDarker: "#007508",
49
+
50
+ secondaryLighter: "#41AAAE",
51
+ secondaryLight: "#20989D",
52
+ secondary: "#018B91",
53
+ secondaryDark: "#016E73",
54
+ secondaryDarker: "#00565A",
55
+
56
+ accentLighter: "#FF625D",
57
+ accentLight: "#FF3732",
58
+ accent: "#F00600",
59
+ accentDark: "#BE0500",
60
+ accentDarker: "#950400",
61
+
62
+ tertiaryLighter: "#FFCF67",
63
+ tertiaryLight: "#FFC23E",
64
+ tertiary: "#FFB109",
65
+ tertiaryDark: "#CA8A00",
66
+ tertiaryDarker: "#9F6D00",
67
+
68
+ // From the Atlassian templates
69
+ neutral900: "#091E42",
70
+ neutral800: "#172B4D",
71
+ neutral700: "#253858",
72
+ neutral600: "#344563",
73
+ neutral500: "#42526E",
74
+ neutral400: "#505F79",
75
+ neutral300: "#5E6C84",
76
+ neutral200: "#6B778C",
77
+ neutral100: "#7A869A",
78
+ neutral90: "#8993A4",
79
+ neutral80: "#97A0AF",
80
+ neutral70: "#A5ADBA",
81
+ neutral60: "#B3BAC5",
82
+ neutral50: "#C1C7D0",
83
+ neutral40: "#DFE1E6",
84
+ neutral30: "#EBECF0",
85
+ neutral20: "#F4F5F7",
86
+ neutral10: "#FAFBFC",
87
+
88
+ primaryFont: DEFAULT_FONT,
89
+ primaryBoldFont: DEFAULT_BOLD_FONT,
90
+
91
+ secondaryFont: DEFAULT_FONT,
92
+ secondaryBoldFont: DEFAULT_BOLD_FONT,
93
+
94
+ accentFont: DEFAULT_FONT,
95
+ accentBoldFont: DEFAULT_BOLD_FONT,
96
+
97
+ buttonFont: DEFAULT_FONT,
98
+ titleFont: DEFAULT_FONT,
99
+ };
100
+
101
+ export type PlatformOS = "ios" | "android" | "web";
102
+
103
+ const fontKeys = [
104
+ "primaryFont",
105
+ "primaryBoldFont",
106
+ "secondaryFont",
107
+ "secondaryBoldFont",
108
+ "buttonFont",
109
+ "accentFont",
110
+ "accentBoldFont",
111
+ "titleFont",
112
+ ];
113
+
114
+ type Luminance = "light" | "lighter" | "dark" | "darker";
115
+ const luminances: Luminance[] = ["lighter", "light", "dark", "darker"];
116
+
117
+ // Changes a color luminance
118
+ export function changeColorLuminance(hex: string, luminanceChange: Luminance) {
119
+ // Validate hex string, strip "#" if present.
120
+ hex = String(hex).replace(/[^0-9a-f]/gi, "");
121
+ if (hex.length === 3) {
122
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
123
+ } else if (hex.length !== 6) {
124
+ throw new Error(`Invalid color hex: ${hex}`);
125
+ }
126
+ let luminance;
127
+ switch (luminanceChange) {
128
+ case "light":
129
+ luminance = -0.2;
130
+ break;
131
+ case "lighter":
132
+ luminance = -0.33;
133
+ break;
134
+ case "dark":
135
+ luminance = 0.2;
136
+ break;
137
+ case "darker":
138
+ luminance = 0.33;
139
+ break;
140
+ default:
141
+ throw new Error(`Cannot change luminance to ${luminanceChange}`);
142
+ }
143
+
144
+ // Convert to decimal and change luminosity
145
+ let rgb = "#";
146
+ for (let i = 0; i < 3; i++) {
147
+ const decimal = parseInt(hex.substr(i * 2, 2), 16);
148
+ const appliedLuminance = Math.round(
149
+ Math.min(Math.max(0, decimal + decimal * luminance), 255)
150
+ ).toString(16);
151
+ // 0 pad, if necessary.
152
+ rgb += `00${appliedLuminance}`.substr(appliedLuminance.length);
153
+ }
154
+
155
+ return rgb;
156
+ }
157
+
158
+ class UnifierClass {
159
+ private _theme?: Partial<UnifiedTheme>;
160
+
161
+ private _web = false;
162
+
163
+ private _dev = false;
164
+
165
+ get web(): boolean {
166
+ return this._web;
167
+ }
168
+
169
+ get dev(): boolean {
170
+ return this._dev;
171
+ }
172
+
173
+ private get fontMap() {
174
+ const fontMap: {[id: string]: FontSource} = {};
175
+ for (const key of fontKeys) {
176
+ if (typeof this.theme[key as keyof typeof Unifier.theme] === "string") {
177
+ fontMap[key] = key;
178
+ } else {
179
+ fontMap[(this.theme as any)[key].name] = (this.theme as any)[key].source;
180
+ }
181
+ }
182
+ return fontMap;
183
+ }
184
+
185
+ // If the theme only has e.g. "primary" set, calculate the primaryLighter, primaryLight, etc based on that color.
186
+ private calculateLuminances(
187
+ theme: Partial<UnifiedTheme>,
188
+ color: "primary" | "secondary" | "accent" | "tertiary"
189
+ ) {
190
+ if (!theme[color]) {
191
+ return;
192
+ }
193
+ for (const luminance of luminances) {
194
+ const capitalized = capitalize(luminance);
195
+ if (!theme[`${color}${capitalized}` as keyof typeof Unifier.theme] && theme[color]) {
196
+ theme[`${color}${capitalized}` as keyof typeof Unifier.theme] = changeColorLuminance(
197
+ theme[color] as string,
198
+ luminance
199
+ );
200
+ }
201
+ }
202
+ }
203
+
204
+ setTheme(theme: Partial<UnifiedTheme>) {
205
+ Unifier.calculateLuminances(theme, "primary");
206
+ Unifier.calculateLuminances(theme, "secondary");
207
+ Unifier.calculateLuminances(theme, "accent");
208
+ Unifier.calculateLuminances(theme, "tertiary");
209
+ this._theme = theme;
210
+ }
211
+
212
+ get theme(): UnifiedTheme {
213
+ return {
214
+ ...DefaultTheme,
215
+ // Custom per project
216
+ primaryLighter: this._theme?.primaryLighter || this._theme?.primary || DefaultTheme.primary,
217
+ primaryLight: this._theme?.primaryLight || this._theme?.primary || DefaultTheme.primary,
218
+ primary: this._theme?.primary || this._theme?.primary || DefaultTheme.primary,
219
+ primaryDark: this._theme?.primaryDark || this._theme?.primary || DefaultTheme.primary,
220
+ primaryDarker: this._theme?.primaryDarker || this._theme?.primary || DefaultTheme.primary,
221
+
222
+ secondaryLighter:
223
+ this._theme?.secondaryLighter || this._theme?.secondary || DefaultTheme.secondary,
224
+ secondaryLight:
225
+ this._theme?.secondaryLight || this._theme?.secondary || DefaultTheme.secondary,
226
+ secondary: this._theme?.secondary || this._theme?.secondary || DefaultTheme.secondary,
227
+ secondaryDark: this._theme?.secondaryDark || this._theme?.secondary || DefaultTheme.secondary,
228
+ secondaryDarker:
229
+ this._theme?.secondaryDarker || this._theme?.secondary || DefaultTheme.secondary,
230
+
231
+ accentLighter: this._theme?.accentLighter || this._theme?.accent || DefaultTheme.accent,
232
+ accentLight: this._theme?.accentLight || this._theme?.accent || DefaultTheme.accent,
233
+ accent: this._theme?.accent || this._theme?.accent || DefaultTheme.accent,
234
+ accentDark: this._theme?.accentDark || this._theme?.accent || DefaultTheme.accent,
235
+ accentDarker: this._theme?.accentDarker || this._theme?.accent || DefaultTheme.accent,
236
+
237
+ tertiaryLighter: this._theme?.tertiaryLighter || this._theme?.accent || DefaultTheme.accent,
238
+ tertiaryLight: this._theme?.tertiaryLight || this._theme?.accent || DefaultTheme.accent,
239
+ tertiary: this._theme?.tertiary || this._theme?.accent || DefaultTheme.accent,
240
+ tertiaryDark: this._theme?.tertiaryDark || this._theme?.accent || DefaultTheme.accent,
241
+ tertiaryDarker: this._theme?.tertiaryDarker || this._theme?.accent || DefaultTheme.accent,
242
+
243
+ primaryFont: this._theme?.primaryFont || DefaultTheme.primaryFont,
244
+ primaryBoldFont:
245
+ this._theme?.primaryBoldFont || this._theme?.primaryFont || DefaultTheme.primaryBoldFont,
246
+ secondaryFont:
247
+ this._theme?.secondaryFont || this._theme?.primaryFont || DefaultTheme.secondaryFont,
248
+ secondaryBoldFont:
249
+ this._theme?.secondaryBoldFont ||
250
+ this._theme?.primaryFont ||
251
+ DefaultTheme.secondaryBoldFont,
252
+ buttonFont: this._theme?.buttonFont || this._theme?.primaryFont || DefaultTheme.buttonFont,
253
+ accentFont: this._theme?.accentFont || this._theme?.primaryFont || DefaultTheme.accentFont,
254
+ accentBoldFont:
255
+ this._theme?.accentBoldFont || this._theme?.primaryFont || DefaultTheme.accentBoldFont,
256
+ titleFont: this._theme?.titleFont || this._theme?.primaryFont || DefaultTheme.titleFont,
257
+ };
258
+ }
259
+
260
+ constructor() {
261
+ console.debug("[unifier] Setting up Unifier");
262
+ }
263
+
264
+ navigation = {
265
+ dismissOverlay: () => {
266
+ console.warn("Dismiss overlay not supported.");
267
+ },
268
+ };
269
+
270
+ loadFonts = async () => {
271
+ try {
272
+ await Font.loadAsync(this.fontMap);
273
+ } catch (err) {
274
+ console.error(`[unifier] Fonts failed to load: ${err}`);
275
+ }
276
+ };
277
+
278
+ // tracking: Tracking,
279
+ utils = {
280
+ dismissKeyboard: () => {
281
+ Keyboard.dismiss();
282
+ },
283
+ dimensions: () => ({
284
+ width: Dimensions.get("window").width,
285
+ height: Dimensions.get("window").height,
286
+ }),
287
+ copyToClipboard: (text: string) => {
288
+ Clipboard.setString(text);
289
+ },
290
+ orientationChange: (callback: (orientation: "portrait" | "landscape") => void) => {
291
+ Dimensions.addEventListener("change", () => {
292
+ const screen = Dimensions.get("screen");
293
+ const isPortrait = screen.width < screen.height;
294
+ callback(isPortrait ? "portrait" : "landscape");
295
+ });
296
+ },
297
+ requestPermissions: async (_perm: PermissionKind) => {
298
+ return requestPermissions(_perm);
299
+ // return requestPermissions(perm);
300
+ },
301
+ makePurchase: () => {
302
+ console.warn("Make purchase not supported yet.");
303
+ },
304
+ PaymentService: () => {
305
+ console.warn("Make purchase not supported yet.");
306
+ },
307
+ vibrate: (pattern?: number[]) => {
308
+ Vibration.vibrate(pattern || [100], false);
309
+ },
310
+ haptic: () => {
311
+ if (Platform.OS !== "web") {
312
+ return Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
313
+ }
314
+ return;
315
+ },
316
+ openUrl: async (url: string) => {
317
+ return Linking.openURL(url);
318
+ },
319
+ // keepAwake: (activate: boolean) => {
320
+ // if (activate) {
321
+ // activateKeepAwake();
322
+ // } else {
323
+ // deactivateKeepAwake();
324
+ // }
325
+ // },
326
+ };
327
+
328
+ storage = {
329
+ getItem: async (key: string, defaultValue?: any) => {
330
+ try {
331
+ const jsonValue = await AsyncStorage.getItem(key);
332
+ if (jsonValue) {
333
+ const value = JSON.parse(jsonValue);
334
+ if (value === null || value === undefined) {
335
+ return defaultValue;
336
+ } else {
337
+ return value;
338
+ }
339
+ } else if (defaultValue !== undefined) {
340
+ return defaultValue;
341
+ } else {
342
+ return null;
343
+ }
344
+ } catch (e) {
345
+ console.error(`[storage] Error reading ${key}`, e);
346
+ return defaultValue || null;
347
+ }
348
+ },
349
+ setItem: async (key: string, item: any) => {
350
+ try {
351
+ const jsonValue = JSON.stringify(item);
352
+ await AsyncStorage.setItem(key, jsonValue);
353
+ } catch (e) {
354
+ console.error(`[storage] Error storing ${key}`, item, e);
355
+ throw new Error(e as any);
356
+ }
357
+ },
358
+ };
359
+
360
+ tracking = {
361
+ log: (message: string) => {
362
+ console.info(message);
363
+ },
364
+ };
365
+
366
+ initIcons = () => {
367
+ console.debug("[unifier] Initializing icons");
368
+ };
369
+ }
370
+
371
+ export const Unifier = new UnifierClass();