commit d94d0b188b34f93ae4705428e9857b134fcd0c45 Author: Bas van Rossem Date: Wed Jun 17 10:19:33 2026 +0200 Initial commit: code as received (Create/Anything export) Insole-production time tracker exported from the Create/Anything AI platform. Baseline snapshot before any reverse-engineering or cleanup. - apps/mobile: Expo Router app (iOS/Android/web), the only workspace - publisher/: standalone OpenNext/AWS deploy tooling for the web side - Backend (/api/tasks, /api/logs + DB) lives remotely, not in this repo diff --git a/.easignore b/.easignore new file mode 100644 index 0000000..4709527 --- /dev/null +++ b/.easignore @@ -0,0 +1,41 @@ +# Applied when EAS bundles the workspace root for an iOS build of apps/mobile +# (see apps/flux/core/src/services/interactive-terminal/create-shell.ts). +# Keep yarn-workspace context (root package.json, yarn.lock, .yarnrc.yml, +# .yarn/patches/) and apps/mobile/ — exclude everything else so EAS uploads +# stay small. + +node_modules/ +**/node_modules/ +.yarn/cache/ +.yarn/install-state.gz +.yarn/unplugged/ + +apps/web/ + +config/ +shared/ +# Re-include the metro polyfills directory: `shared/` above is unanchored and +# would otherwise match `apps/mobile/polyfills/shared/`, which holds the +# expo-image and empty-component shims that metro.config.js redirects to via +# SHARED_ALIASES. Without these files in the EAS upload, EAGER_BUNDLE fails +# with `Unable to resolve module expo-image` on every file that imports it. +!apps/mobile/polyfills/shared/ +examples/ +playwright-report/ +test-results/ +caches/ + +Dockerfile +docker-compose.test.yml +README.md +.dockerignore +.eslintignore +.gitignore +.oxfmtrc.json +.oxlintrc.json + +*.log +*.tgz +.env +.env.* +.DS_Store diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e59f9e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# dependencies (yarn install) +node_modules +**/node_modules +package-lock.json +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +e2b.local.toml +e2b.*.local.toml + +**/.react-router +apps/mobile/caches/* + +# anything +.anything + +# playwright +playwright-report +test-results + diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..01ad14f --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,15 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "always", + "endOfLine": "lf", + "ignorePatterns": ["examples/**/*"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..af76986 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript"], + "rules": { + "eslint/no-const-assign": "deny", + "eslint/constructor-super": "deny", + "eslint/no-this-before-super": "deny", + "eslint/no-obj-calls": "deny", + "eslint/no-new-native-nonconstructor": "deny", + "eslint/no-unsafe-optional-chaining": "deny", + "eslint/no-import-assign": "deny", + "eslint/valid-typeof": "deny", + "react/jsx-key": "deny", + "react/jsx-no-undef": "deny", + "react/no-direct-mutation-state": "deny", + "react/exhaustive-deps": "deny", + "typescript/no-misused-spread": "deny", + "oxc/missing-throw": "deny", + "oxc/uninvoked-array-callback": "deny" + } +} diff --git a/.yarn/patches/@expo+cli+54.0.1.patch b/.yarn/patches/@expo+cli+54.0.1.patch new file mode 100644 index 0000000..debae97 --- /dev/null +++ b/.yarn/patches/@expo+cli+54.0.1.patch @@ -0,0 +1,15 @@ +diff --git a/build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware.js b/build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware.js +index b5cba1b..2cbb2a5 100644 +--- a/build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware.js ++++ b/build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware.js +@@ -143,7 +143,9 @@ class ExpoGoManifestHandlerMiddleware extends _ManifestMiddleware.ManifestMiddle + codeSigningInfo + }); + const expoUpdatesManifest = { +- id: _crypto().default.randomUUID(), ++ id: _crypto().default.randomUUID({ ++ disableEntropyCache: true ++ }), + createdAt: new Date().toISOString(), + runtimeVersion, + launchAsset: { diff --git a/.yarn/patches/@expo+metro-runtime+6.1.2.patch b/.yarn/patches/@expo+metro-runtime+6.1.2.patch new file mode 100644 index 0000000..4ac6b52 --- /dev/null +++ b/.yarn/patches/@expo+metro-runtime+6.1.2.patch @@ -0,0 +1,24 @@ +diff --git a/src/error-overlay/ErrorOverlay.tsx b/src/error-overlay/ErrorOverlay.tsx +index 983dc52..bbe737c 100644 +--- a/src/error-overlay/ErrorOverlay.tsx ++++ b/src/error-overlay/ErrorOverlay.tsx +@@ -30,6 +30,7 @@ const HEADER_TITLE_MAP = { + export function LogBoxInspectorContainer() { + const { selectedLogIndex, logs } = useLogs(); + const log = logs[selectedLogIndex]; ++ return null; + if (log == null) { + return null; + } +diff --git a/src/error-overlay/toast/ErrorToast.tsx b/src/error-overlay/toast/ErrorToast.tsx +index 87a0c8b..c044c8f 100644 +--- a/src/error-overlay/toast/ErrorToast.tsx ++++ b/src/error-overlay/toast/ErrorToast.tsx +@@ -34,6 +34,7 @@ export function ErrorToast(props: Props) { + + useSymbolicatedLog(log); + ++ return null; + return ( + + diff --git a/.yarn/patches/@react-native-community+netinfo+11.4.1.patch b/.yarn/patches/@react-native-community+netinfo+11.4.1.patch new file mode 100644 index 0000000..adc2d8a --- /dev/null +++ b/.yarn/patches/@react-native-community+netinfo+11.4.1.patch @@ -0,0 +1,42 @@ +diff --git a/src/internal/nativeInterface.ts b/src/internal/nativeInterface.ts +index 8b514f4..9135364 100644 +--- a/src/internal/nativeInterface.ts ++++ b/src/internal/nativeInterface.ts +@@ -7,28 +7,15 @@ + * @format + */ + +-import {NativeEventEmitter} from 'react-native'; ++import { NativeEventEmitter } from 'react-native'; + import RNCNetInfo from './nativeModule'; + +-// Produce an error if we don't have the native module +-if (!RNCNetInfo) { +- throw new Error(`@react-native-community/netinfo: NativeModule.RNCNetInfo is null. To fix this issue try these steps: +- +-• Run \`react-native link @react-native-community/netinfo\` in the project root. +-• Rebuild and re-run the app. +-• If you are using CocoaPods on iOS, run \`pod install\` in the \`ios\` directory and then rebuild and re-run the app. You may also need to re-open Xcode to get the new pods. +-• Check that the library was linked correctly when you used the link command by running through the manual installation instructions in the README. +-* If you are getting this error while unit testing you need to mock the native module. Follow the guide in the README. +- +-If none of these fix the issue, please open an issue on the Github repository: https://github.com/react-native-community/react-native-netinfo`); +-} +- + /** + * We export the native interface in this way to give easy shared access to it between the + * JavaScript code and the tests + */ + let nativeEventEmitter: NativeEventEmitter | null = null; +-const nativeInterface = Object.assign(RNCNetInfo, { ++const nativeInterface = RNCNetInfo ? Object.assign(RNCNetInfo, { + get eventEmitter(): NativeEventEmitter { + if (!nativeEventEmitter) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment +@@ -39,5 +26,5 @@ const nativeInterface = Object.assign(RNCNetInfo, { + /// @ts-ignore + return nativeEventEmitter; + }, +-}); ++}) : {}; + export default nativeInterface; diff --git a/.yarn/patches/expo-router+6.0.11.patch b/.yarn/patches/expo-router+6.0.11.patch new file mode 100644 index 0000000..1da7e25 --- /dev/null +++ b/.yarn/patches/expo-router+6.0.11.patch @@ -0,0 +1,506 @@ +diff --git a/assets/native-tabs.module.css b/assets/native-tabs.module.css +index f29cec5..0d71dad 100644 +--- a/assets/native-tabs.module.css ++++ b/assets/native-tabs.module.css +@@ -22,22 +22,27 @@ + } + + .navigationMenuRoot { +- top: 24px; ++ bottom: 24px; + left: 50%; + transform: translateX(-50%); + position: fixed; + z-index: 10; + display: flex; +- background-color: var(--expo-router-tabs-background-color, #272727); +- height: 40px; +- border-radius: 25px; ++ background-color: var(--expo-router-tabs-background-color, rgba(30, 30, 30, 0.88)); ++ backdrop-filter: blur(20px); ++ -webkit-backdrop-filter: blur(20px); ++ height: 56px; ++ border-radius: 28px; + align-items: center; +- justify-content: flex-start; +- padding: 5px; ++ justify-content: center; ++ padding: 4px; + box-sizing: border-box; + margin: 0; +- max-width: 90vw; +- overflow-x: auto; ++ max-width: 95vw; ++ overflow: hidden; ++ border: 1px solid rgba(255, 255, 255, 0.12); ++ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08); ++ gap: 2px; + } + + .navigationMenuTrigger { +@@ -48,36 +53,64 @@ + height: 100%; + background-color: transparent; + border: none; +- margin: 0; +- height: 100%; +- border-radius: 20px; +- padding: 0 20px; ++ border-radius: 24px; ++ padding: 6px 16px; ++ transition: background-color 0.2s ease, backdrop-filter 0.2s ease; + cursor: pointer; +- outline-color: var(--expo-router-tabs-tab-outline-color, #444444); ++ outline-color: var(--expo-router-tabs-tab-outline-color, rgba(255, 255, 255, 0.2)); + position: relative; ++ display: flex; ++ flex-direction: column; ++ align-items: center; ++ justify-content: center; ++ gap: 2px; ++} ++ ++.tabIcon { ++ display: flex; ++ align-items: center; ++ justify-content: center; ++ color: var(--expo-router-tabs-icon-color, rgba(255, 255, 255, 0.6)); ++ font-size: 18px; ++ width: 20px; ++ height: 20px; ++} ++ ++.tabIcon > * { ++ color: inherit; ++} ++ ++.navigationMenuTrigger[data-state="active"] .tabIcon { ++ color: var(--expo-router-tabs-active-icon-color, #ffffff); + } + + .navigationMenuTrigger[data-state="active"] { +- background-color: var(--expo-router-tabs-active-background-color, #444444); ++ background-color: var(--expo-router-tabs-active-background-color, rgba(255, 255, 255, 0.15)); ++ backdrop-filter: blur(10px); ++ -webkit-backdrop-filter: blur(10px); ++ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + + .tabText { + font-weight: var(--expo-router-tabs-font-weight, 500); +- font-size: var(--expo-router-tabs-font-size, 15px); ++ font-size: var(--expo-router-tabs-font-size, 11px); + font-family: var(--expo-router-tabs-font-family); + font-style: var(--expo-router-tabs-font-style, normal); + opacity: var(--expo-router-tabs-text-opacity, 1); +- color: var(--expo-router-tabs-text-color, #8b8b8b); ++ color: var(--expo-router-tabs-text-color, rgba(255, 255, 255, 0.6)); + white-space: nowrap; + } + + .navigationMenuTrigger[data-state="active"] .tabText { + color: var(--expo-router-tabs-active-text-color, #ffffff); +- font-size: var(--expo-router-tabs-active-font-size, var(--expo-router-tabs-font-size, 15px)); ++} ++ ++.navigationMenuTrigger:not([data-state="active"]):hover { ++ background-color: rgba(255, 255, 255, 0.08); + } + + .navigationMenuTrigger:not([data-state="active"]) .tabText:hover { +- opacity: var(--expo-router-tabs-text-hover-opacity, 0.6); ++ opacity: var(--expo-router-tabs-text-hover-opacity, 0.9); + } + + .tabBadge { +@@ -107,3 +140,101 @@ + min-height: var(--expo-router-tabs-local-badge-size); + border-radius: calc(var(--expo-router-tabs-local-badge-size) / 2); + } ++ ++/* More screen styles - Apple Settings inspired */ ++.moreScreen { ++ flex: 1; ++ display: flex; ++ flex-direction: column; ++ background-color: #000000; ++ max-height: 100%; ++ max-width: 100%; ++ overflow-y: auto; ++ padding-bottom: 120px; ++} ++ ++.moreScreenHeader { ++ padding: 60px 20px 8px; ++} ++ ++.moreScreenTitle { ++ font-family: var(--expo-router-tabs-font-family); ++ font-size: 34px; ++ font-weight: 700; ++ color: #ffffff; ++ margin: 0; ++ letter-spacing: 0.37px; ++} ++ ++.moreScreenContent { ++ display: flex; ++ flex-direction: column; ++ padding: 20px 20px 0; ++} ++ ++.moreScreenGroup { ++ background-color: #1c1c1e; ++ border-radius: 10px; ++ overflow: hidden; ++} ++ ++.moreScreenItem { ++ display: flex; ++ align-items: center; ++ gap: 12px; ++ padding: 8px 16px 8px 12px; ++ min-height: 44px; ++ border: none; ++ background-color: transparent; ++ cursor: pointer; ++ font-family: var(--expo-router-tabs-font-family); ++ text-align: left; ++ transition: background-color 0.1s ease; ++ position: relative; ++ width: 100%; ++ box-sizing: border-box; ++} ++ ++.moreScreenItem:hover { ++ background-color: rgba(255, 255, 255, 0.08); ++} ++ ++.moreScreenItem:active { ++ background-color: rgba(255, 255, 255, 0.12); ++} ++ ++/* Separator line between items */ ++.moreScreenItem:not(:last-child)::after { ++ content: ''; ++ position: absolute; ++ bottom: 0; ++ left: 54px; ++ right: 0; ++ height: 0.5px; ++ background-color: rgba(84, 84, 88, 0.65); ++} ++ ++.moreScreenItemIcon { ++ display: flex; ++ align-items: center; ++ justify-content: center; ++ width: 30px; ++ height: 30px; ++ border-radius: 6px; ++ background: linear-gradient(180deg, rgba(255, 255, 255, 0.17) 0%, rgba(255, 255, 255, 0) 100%), #636366; ++ color: #ffffff; ++ flex-shrink: 0; ++} ++ ++.moreScreenItemLabel { ++ flex: 1; ++ font-size: 17px; ++ font-weight: 400; ++ color: #ffffff; ++ letter-spacing: -0.41px; ++} ++ ++.moreScreenItemChevron { ++ color: rgba(235, 235, 245, 0.3); ++ flex-shrink: 0; ++} +diff --git a/build/getLinkingConfig.js b/build/getLinkingConfig.js +index 2a0ee73..302afc0 100644 +--- a/build/getLinkingConfig.js ++++ b/build/getLinkingConfig.js +@@ -8,6 +8,7 @@ const constants_1 = require("./constants"); + const getReactNavigationConfig_1 = require("./getReactNavigationConfig"); + const getRoutesRedirects_1 = require("./getRoutesRedirects"); + const linking_1 = require("./link/linking"); ++const isAnythingApp = expo_modules_core_1.Platform.OS === 'ios' && !globalThis.expo?.modules?.ExpoGo; + function getNavigationConfig(routes, metaOnly, { sitemap, notFound }) { + const config = (0, getReactNavigationConfig_1.getReactNavigationConfig)(routes, metaOnly); + const sitemapRoute = (() => { +@@ -61,7 +62,10 @@ function getLinkingConfig(routes, context, getRouteInfo, { metaOnly = true, serv + // Expo Router calls `getInitialURL` twice, which may confuse the user if they provide a custom `getInitialURL`. + // Therefor we memoize the result. + if (!hasCachedInitialUrl) { +- if (expo_modules_core_1.Platform.OS === 'web') { ++ if (isAnythingApp) { ++ initialUrl = '/'; ++ } ++ else if (expo_modules_core_1.Platform.OS === 'web') { + initialUrl = serverUrl ?? (0, linking_1.getInitialURL)(); + } + else { +diff --git a/build/link/linking.js b/build/link/linking.js +index b9535b5..ec99c96 100644 +--- a/build/link/linking.js ++++ b/build/link/linking.js +@@ -47,6 +47,7 @@ Object.defineProperty(exports, "getStateFromPath", { enumerable: true, get: func + const useLinking_1 = require("../fork/useLinking"); + const getRoutesRedirects_1 = require("../getRoutesRedirects"); + const isExpoGo = typeof expo !== 'undefined' && globalThis.expo?.modules?.ExpoGo; ++const isAnythingApp = react_native_1.Platform.OS === 'ios' && !globalThis.expo?.modules?.ExpoGo; + // A custom getInitialURL is used on native to ensure the app always starts at + // the root path if it's launched from something other than a deep link. + // This helps keep the native functionality working like the web functionality. +@@ -124,7 +125,12 @@ function subscribe(nativeLinking, redirects) { + } + }; + } +- const subscription = Linking.addEventListener('url', callback); ++ let subscription; ++ ++ if (!isAnythingApp) { ++ subscription = Linking.addEventListener('url', callback); ++ } ++ + return () => { + // https://github.com/facebook/react-native/commit/6d1aca806cee86ad76de771ed3a1cc62982ebcd7 + subscription?.remove?.(); +diff --git a/build/native-tabs/NativeBottomTabs/NativeTabTrigger.js b/build/native-tabs/NativeBottomTabs/NativeTabTrigger.js +index cd3d597..8bc0b00 100644 +--- a/build/native-tabs/NativeBottomTabs/NativeTabTrigger.js ++++ b/build/native-tabs/NativeBottomTabs/NativeTabTrigger.js +@@ -129,6 +129,16 @@ function appendLabelOptions(options, props) { + else { + options.title = props.children; + options.selectedLabelStyle = props.selectedStyle; ++ // Extract label color for web ++ if (props.style?.color) { ++ options.webLabelColor = props.style.color; ++ } ++ if (props.color) { ++ options.webLabelColor = props.color; ++ } ++ if (props.selectedStyle?.color) { ++ options.webLabelSelectedColor = props.selectedStyle.color; ++ } + } + } + function appendIconOptions(options, props) { +@@ -136,6 +146,21 @@ function appendIconOptions(options, props) { + const icon = convertIconSrcToIconOption(props); + options.icon = icon?.icon; + options.selectedIcon = icon?.selectedIcon; ++ // Preserve icon info for web rendering ++ const srcValue = typeof props.src === 'object' && 'selected' in props.src ? props.src.default : props.src; ++ if ((0, react_1.isValidElement)(srcValue) && srcValue.type === elements_1.VectorIcon) { ++ options.webIconFamily = srcValue.props.family; ++ options.webIconName = srcValue.props.name; ++ // Extract colors from VectorIcon props for web ++ if (srcValue.props.color) { ++ options.webIconColor = srcValue.props.color; ++ } ++ if (srcValue.props.selectedColor) { ++ options.webIconSelectedColor = srcValue.props.selectedColor; ++ } ++ } else { ++ options.webIcon = srcValue; ++ } + } + else if ('sf' in props && process.env.EXPO_OS === 'ios') { + if (typeof props.sf === 'string') { +@@ -169,6 +194,13 @@ function appendIconOptions(options, props) { + options.selectedIcon = undefined; + } + options.selectedIconColor = props.selectedColor; ++ // Extract icon color for web ++ if (props.color) { ++ options.webIconColor = props.color; ++ } ++ if (props.selectedColor) { ++ options.webIconSelectedColor = props.selectedColor; ++ } + } + function convertIconSrcToIconOption(icon) { + if (icon && icon.src) { +diff --git a/build/native-tabs/NativeBottomTabs/NativeTabsView.web.js b/build/native-tabs/NativeBottomTabs/NativeTabsView.web.js +index d3d738b..a27f83d 100644 +--- a/build/native-tabs/NativeBottomTabs/NativeTabsView.web.js ++++ b/build/native-tabs/NativeBottomTabs/NativeTabsView.web.js +@@ -41,16 +41,45 @@ const react_tabs_1 = require("@radix-ui/react-tabs"); + const react_1 = __importStar(require("react")); + const utils_1 = require("./utils"); + const native_tabs_module_css_1 = __importDefault(require("../../../assets/native-tabs.module.css")); ++ ++const MAX_VISIBLE_TABS = 5; ++ + function NativeTabsView(props) { + const { builder, focusedIndex } = props; + const { state, descriptors, navigation } = builder; + const { routes } = state; ++ const [showMoreScreen, setShowMoreScreen] = (0, react_1.useState)(false); + const defaultTabName = (0, react_1.useMemo)(() => state.routes[focusedIndex]?.name ?? state.routes[0].name, []); + const value = state.routes[focusedIndex]?.name ?? state.routes[0].name; + const currentTabKey = state.routes[focusedIndex]?.key ?? state.routes[0].key; +- const items = routes +- .filter(({ key }) => (0, utils_1.shouldTabBeVisible)(descriptors[key].options)) +- .map((route) => ()); ++ ++ const visibleRoutes = routes.filter(({ key }) => (0, utils_1.shouldTabBeVisible)(descriptors[key].options)); ++ const hasOverflow = visibleRoutes.length > MAX_VISIBLE_TABS; ++ const primaryRoutes = hasOverflow ? visibleRoutes.slice(0, MAX_VISIBLE_TABS - 1) : visibleRoutes; ++ const overflowRoutes = hasOverflow ? visibleRoutes.slice(MAX_VISIBLE_TABS - 1) : []; ++ ++ // Check if an overflow tab is currently active ++ const isOverflowTabActive = overflowRoutes.some(route => route.name === value); ++ ++ const items = primaryRoutes.map((route) => ( ++ setShowMoreScreen(false)} ++ forceInactive={showMoreScreen} ++ isActive={route.name === value && !showMoreScreen} ++ /> ++ )); ++ + const children = routes + .filter(({ key }) => (0, utils_1.shouldTabBeVisible)(descriptors[key].options)) + .map((route) => { +@@ -58,26 +87,116 @@ function NativeTabsView(props) { + {descriptors[route.key].render()} + ); + }); +- return ( { +- navigation.dispatch({ +- type: 'JUMP_TO', +- target: state.key, +- payload: { +- name: value, +- }, +- }); ++ ++ const handleNavigate = (routeName) => { ++ navigation.dispatch({ ++ type: 'JUMP_TO', ++ target: state.key, ++ payload: { ++ name: routeName, ++ }, ++ }); ++ setShowMoreScreen(false); ++ }; ++ ++ return ( { ++ handleNavigate(newValue); + }} style={convertNativeTabsPropsToStyleVars(props, descriptors[currentTabKey]?.options)}> ++ ++ {/* More Screen - shown when More tab is active */} ++ {showMoreScreen && ( ++
++
++

More

++
++
++
++ {overflowRoutes.map((route) => ( ++ ++ ))} ++
++
++
++ )} ++ ++ {/* Tab Content - hidden when More screen is shown */} ++ {!showMoreScreen && children} ++ + + {items} ++ {hasOverflow && ( ++ ++ )} + +- {children} +
); + } ++ ++function OverflowTabIcon(props) { ++ const { webIcon, webIconFamily, webIconName } = props; ++ if (webIconFamily && webIconName) { ++ const IconComponent = webIconFamily; ++ return ; ++ } else if (webIcon) { ++ return webIcon; ++ } ++ return null; ++} ++ + function TabItem(props) { +- const { title, badgeValue, route } = props; ++ const { title, badgeValue, route, webIcon, webIconFamily, webIconName, webIconColor, webIconSelectedColor, webLabelColor, webLabelSelectedColor, onClick, forceInactive, isActive } = props; + const isBadgeEmpty = badgeValue === ' '; +- return ( +- {title} ++ const dataState = forceInactive ? "inactive" : (isActive ? "active" : "inactive"); ++ ++ // Resolve colors based on active state ++ const resolvedIconColor = isActive && webIconSelectedColor ? webIconSelectedColor : webIconColor; ++ const resolvedLabelColor = isActive && webLabelSelectedColor ? webLabelSelectedColor : webLabelColor; ++ ++ const iconStyle = resolvedIconColor ? { color: resolvedIconColor } : {}; ++ const labelStyle = resolvedLabelColor ? { color: resolvedLabelColor } : {}; ++ ++ let iconElement = null; ++ if (webIconFamily && webIconName) { ++ const IconComponent = webIconFamily; ++ iconElement = (); ++ } else if (webIcon) { ++ iconElement = ({webIcon}); ++ } ++ return ( ++ {iconElement} ++ {title} + {badgeValue && (
+ {badgeValue} +
)} diff --git a/.yarn/patches/expo-store-review+9.0.8.patch b/.yarn/patches/expo-store-review+9.0.8.patch new file mode 100644 index 0000000..c7fa56e --- /dev/null +++ b/.yarn/patches/expo-store-review+9.0.8.patch @@ -0,0 +1,375 @@ +diff --git a/build/ExpoStoreReview.d.ts b/build/ExpoStoreReview.d.ts +index 00e8119..ed3992e 100644 +--- a/build/ExpoStoreReview.d.ts ++++ b/build/ExpoStoreReview.d.ts +@@ -1,6 +1,9 @@ + declare const _default: Partial<{ + isAvailableAsync: () => Promise; + requestReview: () => Promise; ++ prePromptReview: () => Promise; ++ resetReviewState: () => Promise; ++ hasUserRated: () => Promise; + }>; + export default _default; + //# sourceMappingURL=ExpoStoreReview.d.ts.map +\ No newline at end of file +diff --git a/build/ExpoStoreReview.d.ts.map b/build/ExpoStoreReview.d.ts.map +index aec682b..0a49993 100644 +--- a/build/ExpoStoreReview.d.ts.map ++++ b/build/ExpoStoreReview.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"ExpoStoreReview.d.ts","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"wBACqB,OAAO,CAAC;IAC3B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAHF,wBAGG"} +\ No newline at end of file ++{"version":3,"file":"ExpoStoreReview.d.ts","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"wBACqB,OAAO,CAAC;IAC3B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC,CAAC;AANF,wBAMG"} +\ No newline at end of file +diff --git a/build/ExpoStoreReview.d.ts.map.orig b/build/ExpoStoreReview.d.ts.map.orig +new file mode 100644 +index 0000000..4e06d74 +--- /dev/null ++++ b/build/ExpoStoreReview.d.ts.map.orig +@@ -0,0 +1 @@ ++{"version":3,"file":"ExpoStoreReview.d.ts","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"wBACqB,OAAO,CAAC;IAC3B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;AALF,wBAKG"} +\ No newline at end of file +diff --git a/build/ExpoStoreReview.d.ts.map.rej b/build/ExpoStoreReview.d.ts.map.rej +new file mode 100644 +index 0000000..93ac91c +--- /dev/null ++++ b/build/ExpoStoreReview.d.ts.map.rej +@@ -0,0 +1,5 @@ ++@@ -1,1 +1,1 @@ ++-{"version":3,"file":"ExpoStoreReview.d.ts","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"wBACqB,OAAO,CAAC;IAC3B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC,CAAC;AAHF,wBAGG"} ++\ No newline at end of line +++{"version":3,"file":"ExpoStoreReview.d.ts","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"wBACqB,OAAO,CAAC;IAC3B,gBAAgB,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,YAAY,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACtC,CAAC;AANF,wBAMG"} ++\ No newline at end of line +diff --git a/build/ExpoStoreReview.js.map b/build/ExpoStoreReview.js.map +index 80fdadb..7fdbdfa 100644 +--- a/build/ExpoStoreReview.js.map ++++ b/build/ExpoStoreReview.js.map +@@ -1 +1 @@ +-{"version":3,"file":"ExpoStoreReview.js","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,eAAe,EAGb,CAAC","sourcesContent":["// Unimplemented on web\nexport default {} as Partial<{\n isAvailableAsync: () => Promise;\n requestReview: () => Promise;\n}>;\n"]} +\ No newline at end of file ++{"version":3,"file":"ExpoStoreReview.js","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,eAAe,EAMb,CAAC","sourcesContent":["// Unimplemented on web\nexport default {} as Partial<{\n isAvailableAsync: () => Promise;\n requestReview: () => Promise;\n prePromptReview: () => Promise;\n resetReviewState: () => Promise;\n hasUserRated: () => Promise;\n}>;\n"]} +\ No newline at end of file +diff --git a/build/ExpoStoreReview.js.map.orig b/build/ExpoStoreReview.js.map.orig +new file mode 100644 +index 0000000..867130c +--- /dev/null ++++ b/build/ExpoStoreReview.js.map.orig +@@ -0,0 +1 @@ ++{"version":3,"file":"ExpoStoreReview.js","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,eAAe,EAKb,CAAC","sourcesContent":["// Unimplemented on web\nexport default {} as Partial<{\n isAvailableAsync: () => Promise;\n requestReview: () => Promise;\n prePromptReview: () => Promise;\n resetReviewState: () => Promise;\n}>;\n"]} +\ No newline at end of file +diff --git a/build/ExpoStoreReview.js.map.rej b/build/ExpoStoreReview.js.map.rej +new file mode 100644 +index 0000000..8a0b07d +--- /dev/null ++++ b/build/ExpoStoreReview.js.map.rej +@@ -0,0 +1,5 @@ ++@@ -1,1 +1,1 @@ ++-{"version":3,"file":"ExpoStoreReview.js","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,eAAe,EAGb,CAAC","sourcesContent":["// Unimplemented on web\nexport default {} as Partial<{\n isAvailableAsync: () => Promise;\n requestReview: () => Promise;\n}>;\n"]} ++\ No newline at end of line +++{"version":3,"file":"ExpoStoreReview.js","sourceRoot":"","sources":["../src/ExpoStoreReview.ts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,eAAe,EAMb,CAAC","sourcesContent":["// Unimplemented on web\nexport default {} as Partial<{\n isAvailableAsync: () => Promise;\n requestReview: () => Promise;\n prePromptReview: () => Promise;\n resetReviewState: () => Promise;\n hasUserRated: () => Promise;\n}>;\n"]} ++\ No newline at end of line +diff --git a/build/ExpoStoreReview.native.js b/build/ExpoStoreReview.native.js +index 39755d3..a110bfe 100644 +--- a/build/ExpoStoreReview.native.js ++++ b/build/ExpoStoreReview.native.js +@@ -1,3 +1,4 @@ + import { requireNativeModule } from 'expo-modules-core'; +-export default requireNativeModule('ExpoStoreReview'); ++ ++export default globalThis && globalThis.expo && globalThis.expo.modules && globalThis.expo.modules.ExpoStoreReview ? requireNativeModule('ExpoStoreReview') : {}; + //# sourceMappingURL=ExpoStoreReview.native.js.map +diff --git a/build/StoreReview.d.ts b/build/StoreReview.d.ts +index 00cf30a..0f91d73 100644 +--- a/build/StoreReview.d.ts ++++ b/build/StoreReview.d.ts +@@ -7,6 +7,25 @@ + * - On Web, it will resolve to `false`. + */ + export declare function isAvailableAsync(): Promise; ++/** ++ * Shows a pre-prompt alert asking the user if they'd like to rate the app. If they select "Yes", ++ * it will then show the native store review prompt. This is useful for improving review conversion rates. ++ * Currently only available on iOS. ++ */ ++export declare function prePromptReview(): Promise; ++/** ++ * Resets the review state stored in UserDefaults. This allows you to clear the ++ * tracking of whether the user has already been prompted for a review. ++ * Currently only available on iOS. ++ */ ++export declare function resetReviewState(): Promise; ++/** ++ * Checks whether the user has already rated the app (i.e., they selected "Rate Now" ++ * in the pre-prompt dialog). This can be used to conditionally show or hide rating prompts. ++ * Currently only available on iOS. ++ * @return A promise that resolves to true if the user has rated, false otherwise. ++ */ ++export declare function hasUserRated(): Promise; + /** + * In ideal circumstances this will open a native modal and allow the user to select a star rating + * that will then be applied to the App Store, without leaving the app. If the device is running +diff --git a/build/StoreReview.d.ts.map b/build/StoreReview.d.ts.map +index 4ce88d2..6f7f508 100644 +--- a/build/StoreReview.d.ts.map ++++ b/build/StoreReview.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"StoreReview.d.ts","sourceRoot":"","sources":["../src/StoreReview.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEzD;AAGD;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAmBnD;AAGD;;;;;GAKG;AACH,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAQxC;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAElD"} +\ No newline at end of file ++{"version":3,"file":"StoreReview.d.ts","sourceRoot":"","sources":["../src/StoreReview.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEzD;AAGD;;;;GAIG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrD;AAGD;;;;GAIG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMtD;AAGD;;;;;GAKG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC,CAOrD;AAGD;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAmBnD;AAGD;;;;;GAKG;AACH,wBAAgB,QAAQ,IAAI,MAAM,GAAG,IAAI,CAQxC;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAElD"} +\ No newline at end of file +diff --git a/build/StoreReview.js b/build/StoreReview.js +index 5ddf98b..232423f 100644 +--- a/build/StoreReview.js ++++ b/build/StoreReview.js +@@ -15,6 +15,47 @@ export async function isAvailableAsync() { + return StoreReview.isAvailableAsync?.() ?? false; + } + // @needsAudit ++/** ++ * Shows a pre-prompt alert asking the user if they'd like to rate the app. If they select "Yes", ++ * it will then show the native store review prompt. This is useful for improving review conversion rates. ++ * Currently only available on iOS. ++ */ ++export async function prePromptReview() { ++ if (StoreReview?.prePromptReview) { ++ return StoreReview.prePromptReview(); ++ } ++ // Fallback: if prePromptReview is not available, just do nothing ++ // (This will be the case on web and Android) ++} ++// @needsAudit ++/** ++ * Resets the review state by clearing the last review request timestamp and rating status. ++ * This is useful for testing or if you want to allow the review prompt to be shown again ++ * regardless of the normal rate limiting. Currently only available on iOS. ++ */ ++export async function resetReviewState() { ++ if (StoreReview?.resetReviewState) { ++ return StoreReview.resetReviewState(); ++ } ++ // Fallback: if resetReviewState is not available, just do nothing ++ // (This will be the case on web and Android) ++} ++// @needsAudit ++/** ++ * Checks whether the user has already rated the app (i.e., they selected "Rate Now" ++ * in the pre-prompt dialog). This can be used to conditionally show or hide rating prompts. ++ * Currently only available on iOS. ++ * @return A promise that resolves to true if the user has rated, false otherwise. ++ */ ++export async function hasUserRated() { ++ if (StoreReview?.hasUserRated) { ++ return StoreReview.hasUserRated(); ++ } ++ // Fallback: if hasUserRated is not available, return false ++ // (This will be the case on web and Android) ++ return false; ++} ++// @needsAudit + /** + * In ideal circumstances this will open a native modal and allow the user to select a star rating + * that will then be applied to the App Store, without leaving the app. If the device is running +diff --git a/build/StoreReview.js.map b/build/StoreReview.js.map +index cd16b26..56882e2 100644 +--- a/build/StoreReview.js.map ++++ b/build/StoreReview.js.map +@@ -1 +1 @@ +-{"version":3,"file":"StoreReview.js","sourceRoot":"","sources":["../src/StoreReview.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,WAAW,MAAM,mBAAmB,CAAC;AAE5C,cAAc;AACd;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,WAAW,CAAC,gBAAgB,EAAE,EAAE,IAAI,KAAK,CAAC;AACnD,CAAC;AAED,cAAc;AACd;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,WAAW,EAAE,aAAa,EAAE,CAAC;QAC/B,OAAO,WAAW,CAAC,aAAa,EAAE,CAAC;IACrC,CAAC;IACD,6GAA6G;IAC7G,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;IACvB,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,qDAAqD,EAAE,GAAG,CAAC,CAAC;QAC3E,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;SAAM,CAAC;QACN,iDAAiD;QACjD,OAAO,CAAC,IAAI,CACV,+JAA+J,CAChK,CAAC;IACJ,CAAC;AACH,CAAC;AAED,cAAc;AACd;;;;;GAKG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC;IACxC,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,UAAU,EAAE,GAAG,EAAE,CAAC;QAC7C,OAAO,UAAU,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;IAC5C,CAAC;SAAM,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,UAAU,EAAE,OAAO,EAAE,CAAC;QAC5D,OAAO,UAAU,CAAC,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,cAAc;AACd;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,OAAO,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,gBAAgB,EAAE,CAAC,CAAC;AACpD,CAAC","sourcesContent":["import Constants from 'expo-constants';\nimport { Platform } from 'expo-modules-core';\nimport { Linking } from 'react-native';\n\nimport StoreReview from './ExpoStoreReview';\n\n// @needsAudit\n/**\n * Determines if the platform has the capabilities to use `StoreReview.requestReview()`.\n * @return\n * This returns a promise fulfills with `boolean`, depending on the platform:\n * - On iOS, it will resolve to `true` unless the app is distributed through TestFlight.\n * - On Android, it will resolve to `true` if the device is running Android 5.0+.\n * - On Web, it will resolve to `false`.\n */\nexport async function isAvailableAsync(): Promise {\n return StoreReview.isAvailableAsync?.() ?? false;\n}\n\n// @needsAudit\n/**\n * In ideal circumstances this will open a native modal and allow the user to select a star rating\n * that will then be applied to the App Store, without leaving the app. If the device is running\n * a version of Android lower than 5.0, this will attempt to get the store URL and link the user to it.\n */\nexport async function requestReview(): Promise {\n if (StoreReview?.requestReview) {\n return StoreReview.requestReview();\n }\n // If StoreReview is unavailable then get the store URL from `app.config.js` or `app.json` and open the store\n const url = storeUrl();\n if (url) {\n const supported = await Linking.canOpenURL(url);\n if (!supported) {\n console.warn(\"StoreReview.requestReview(): Can't open store url: \", url);\n } else {\n await Linking.openURL(url);\n }\n } else {\n // If the store URL is missing, let the dev know.\n console.warn(\n \"StoreReview.requestReview(): Couldn't link to store, please make sure the `android.playStoreUrl` & `ios.appStoreUrl` fields are filled out in your `app.json`\"\n );\n }\n}\n\n// @needsAudit\n/**\n * This uses the `Constants` API to get the `Constants.expoConfig.ios.appStoreUrl` on iOS, or the\n * `Constants.expoConfig.android.playStoreUrl` on Android.\n *\n * On Web this will return `null`.\n */\nexport function storeUrl(): string | null {\n const expoConfig = Constants.expoConfig;\n if (Platform.OS === 'ios' && expoConfig?.ios) {\n return expoConfig.ios.appStoreUrl ?? null;\n } else if (Platform.OS === 'android' && expoConfig?.android) {\n return expoConfig.android.playStoreUrl ?? null;\n }\n return null;\n}\n\n// @needsAudit\n/**\n * @return This returns a promise that fulfills to `true` if `StoreReview.requestReview()` is capable\n * directing the user to some kind of store review flow. If the app config (`app.json`) does not\n * contain store URLs and native store review capabilities are not available then the promise\n * will fulfill to `false`.\n *\n * @example\n * ```ts\n * if (await StoreReview.hasAction()) {\n * // you can call StoreReview.requestReview()\n * }\n * ```\n */\nexport async function hasAction(): Promise {\n return !!storeUrl() || (await isAvailableAsync());\n}\n"]} +\ No newline at end of file ++{"version":3,"file":"StoreReview.js","sourceRoot":"","sources":["../src/StoreReview.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,WAAW,MAAM,mBAAmB,CAAC;AAE5C,cAAc;AACd;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,WAAW,CAAC,gBAAgB,EAAE,EAAE,IAAI,KAAK,CAAC;AACnD,CAAC;AAED,cAAc;AACd;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,WAAW,EAAE,eAAe,EAAE,CAAC;QACjC,OAAO,WAAW,CAAC,eAAe,EAAE,CAAC;IACvC,CAAC;IACD,iEAAiE;IACjE,6CAA6C;AAC/C,CAAC;AAED,cAAc;AACd;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,WAAW,EAAE,gBAAgB,EAAE,CAAC;QAClC,OAAO,WAAW,CAAC,gBAAgB,EAAE,CAAC;IACxC,CAAC;IACD,iEAAiE;IACjE,6CAA6C;AAC/C,CAAC;AAED,cAAc;AACd;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,IAAI,WAAW,EAAE,YAAY,EAAE,CAAC;QAC9B,OAAO,WAAW,CAAC,YAAY,EAAE,CAAC;IACpC,CAAC;IACD,yEAAyE;IACzE,6CAA6C;IAC7C,OAAO,KAAK,CAAC;AACf,CAAC;AAED,cAAc;AACd;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,IAAI,WAAW,EAAE,aAAa,EAAE,CAAC;QAC/B,OAAO,WAAW,CAAC,aAAa,EAAE,CAAC;IACrC,CAAC;IACD,6GAA6G;IAC7G,MAAM,GAAG,GAAG,QAAQ,EAAE,CAAC;IACvB,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,qDAAqD,EAAE,GAAG,CAAC,CAAC;QAC3E,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;SAAM,CAAC;QACN,iDAAiD;QACjD,OAAO,CAAC,IAAI,CACV,+JAA+J,CAChK,CAAC;IACJ,CAAC;AACH,CAAC;AAED,cAAc;AACd;;;;;GAKG;AACH,MAAM,UAAU,QAAQ;IACtB,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC;IACxC,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,UAAU,EAAE,GAAG,EAAE,CAAC;QAC7C,OAAO,UAAU,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;IAC5C,CAAC;SAAM,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,UAAU,EAAE,OAAO,EAAE,CAAC;QAC5D,OAAO,UAAU,CAAC,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,cAAc;AACd;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,OAAO,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,gBAAgB,EAAE,CAAC,CAAC;AACpD,CAAC","sourcesContent":["import Constants from 'expo-constants';\nimport { Platform } from 'expo-modules-core';\nimport { Linking } from 'react-native';\n\nimport StoreReview from './ExpoStoreReview';\n\n// @needsAudit\n/**\n * Determines if the platform has the capabilities to use `StoreReview.requestReview()`.\n * @return\n * This returns a promise fulfills with `boolean`, depending on the platform:\n * - On iOS, it will resolve to `true` unless the app is distributed through TestFlight.\n * - On Android, it will resolve to `true` if the device is running Android 5.0+.\n * - On Web, it will resolve to `false`.\n */\nexport async function isAvailableAsync(): Promise {\n return StoreReview.isAvailableAsync?.() ?? false;\n}\n\n// @needsAudit\n/**\n * Shows a pre-prompt alert asking the user if they'd like to rate the app. If they select \"Yes\",\n * it will then show the native store review prompt. This is useful for improving review conversion rates.\n * Currently only available on iOS.\n */\nexport async function prePromptReview(): Promise {\n if (StoreReview?.prePromptReview) {\n return StoreReview.prePromptReview();\n }\n // Fallback: if prePromptReview is not available, just do nothing\n // (This will be the case on web and Android)\n}\n\n// @needsAudit\n/**\n * Resets the review state stored in UserDefaults. This allows you to clear the\n * tracking of whether the user has already been prompted for a review.\n * Currently only available on iOS.\n */\nexport async function resetReviewState(): Promise {\n if (StoreReview?.resetReviewState) {\n return StoreReview.resetReviewState();\n }\n // Fallback: if resetReviewState is not available, just do nothing\n // (This will be the case on web and Android)\n}\n\n// @needsAudit\n/**\n * Checks whether the user has already rated the app (i.e., they selected \"Rate Now\"\n * in the pre-prompt dialog). This can be used to conditionally show or hide rating prompts.\n * Currently only available on iOS.\n * @return A promise that resolves to true if the user has rated, false otherwise.\n */\nexport async function hasUserRated(): Promise {\n if (StoreReview?.hasUserRated) {\n return StoreReview.hasUserRated();\n }\n // Fallback: if hasUserRated is not available, return false\n // (This will be the case on web and Android)\n return false;\n}\n\n// @needsAudit\n/**\n * In ideal circumstances this will open a native modal and allow the user to select a star rating\n * that will then be applied to the App Store, without leaving the app. If the device is running\n * a version of Android lower than 5.0, this will attempt to get the store URL and link the user to it.\n */\nexport async function requestReview(): Promise {\n if (StoreReview?.requestReview) {\n return StoreReview.requestReview();\n }\n // If StoreReview is unavailable then get the store URL from `app.config.js` or `app.json` and open the store\n const url = storeUrl();\n if (url) {\n const supported = await Linking.canOpenURL(url);\n if (!supported) {\n console.warn(\"StoreReview.requestReview(): Can't open store url: \", url);\n } else {\n await Linking.openURL(url);\n }\n } else {\n // If the store URL is missing, let the dev know.\n console.warn(\n \"StoreReview.requestReview(): Couldn't link to store, please make sure the `android.playStoreUrl` & `ios.appStoreUrl` fields are filled out in your `app.json`\"\n );\n }\n}\n\n// @needsAudit\n/**\n * This uses the `Constants` API to get the `Constants.expoConfig.ios.appStoreUrl` on iOS, or the\n * `Constants.expoConfig.android.playStoreUrl` on Android.\n *\n * On Web this will return `null`.\n */\nexport function storeUrl(): string | null {\n const expoConfig = Constants.expoConfig;\n if (Platform.OS === 'ios' && expoConfig?.ios) {\n return expoConfig.ios.appStoreUrl ?? null;\n } else if (Platform.OS === 'android' && expoConfig?.android) {\n return expoConfig.android.playStoreUrl ?? null;\n }\n return null;\n}\n\n// @needsAudit\n/**\n * @return This returns a promise that fulfills to `true` if `StoreReview.requestReview()` is capable\n * directing the user to some kind of store review flow. If the app config (`app.json`) does not\n * contain store URLs and native store review capabilities are not available then the promise\n * will fulfill to `false`.\n *\n * @example\n * ```ts\n * if (await StoreReview.hasAction()) {\n * // you can call StoreReview.requestReview()\n * }\n * ```\n */\nexport async function hasAction(): Promise {\n return !!storeUrl() || (await isAvailableAsync());\n}\n"]} +\ No newline at end of file +diff --git a/ios/StoreReviewModule.swift b/ios/StoreReviewModule.swift +index c44c468..4eb6ea0 100644 +--- a/ios/StoreReviewModule.swift ++++ b/ios/StoreReviewModule.swift +@@ -1,5 +1,8 @@ + import ExpoModulesCore + import StoreKit ++import UIKit ++ ++private let HAS_RATED_KEY = "anything_has_rated" + + public class StoreReviewModule: Module { + public func definition() -> ModuleDefinition { +@@ -9,11 +12,55 @@ public class StoreReviewModule: Module { + return !isRunningFromTestFlight() + } + ++ AsyncFunction("prePromptReview") { ++ if isRunningFromTestFlight() { ++ return ++ } ++ ++ try await MainActor.run { ++ let defaults = UserDefaults.standard ++ ++ if defaults.bool(forKey: HAS_RATED_KEY) { ++ return ++ } ++ ++ guard let currentScene = getForegroundActiveScene() else { ++ throw MissingCurrentWindowSceneException() ++ } ++ ++ let keyWindow = currentScene.windows.first(where: { $0.isKeyWindow }) ++ guard let rootVC = keyWindow?.rootViewController else { ++ throw MissingCurrentWindowSceneException() ++ } ++ ++ let alert = UIAlertController( ++ title: "Thanks for using Anything!", ++ message: "Share what you love about Anything in the App Store. Your review will help us reach more people.", ++ preferredStyle: .alert ++ ) ++ ++ let noAction = UIAlertAction(title: "No Thanks", style: .cancel, handler: nil) ++ ++ let yesAction = UIAlertAction(title: "Rate Now", style: .default) { _ in ++ defaults.set(true, forKey: HAS_RATED_KEY) ++ ++ if #available(iOS 16.0, *) { ++ AppStore.requestReview(in: currentScene) ++ } else { ++ SKStoreReviewController.requestReview(in: currentScene) ++ } ++ } ++ ++ alert.addAction(noAction) ++ alert.addAction(yesAction) ++ ++ rootVC.present(alert, animated: true, completion: nil) ++ } ++ } ++ + AsyncFunction("requestReview") { + try await MainActor.run { + guard let currentScene = getForegroundActiveScene() else { +- // If no valid foreground scene is found, throw an exception +- // as the review prompt won't be visible in background + throw MissingCurrentWindowSceneException() + } + if #available(iOS 16.0, *) { +@@ -23,23 +70,30 @@ public class StoreReviewModule: Module { + } + } + } ++ ++ AsyncFunction("resetReviewState") { ++ let defaults = UserDefaults.standard ++ defaults.removeObject(forKey: HAS_RATED_KEY) ++ } ++ ++ AsyncFunction("hasUserRated") { () -> Bool in ++ let defaults = UserDefaults.standard ++ return defaults.bool(forKey: HAS_RATED_KEY) ++ } ++ + } + + private func getForegroundActiveScene() -> UIWindowScene? { +- // First try to find a foreground active scene +- if let activeScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { ++ if let activeScene = UIApplication.shared.connectedScenes ++ .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { + return activeScene + } + +- // If no foreground active scene is found (e.g., app is in App Switcher), +- // try to find any foreground inactive scene +- if let foregroundScene = UIApplication.shared.connectedScenes.first(where: { +- $0.activationState == .foregroundInactive +- }) as? UIWindowScene { ++ if let foregroundScene = UIApplication.shared.connectedScenes ++ .first(where: { $0.activationState == .foregroundInactive }) as? UIWindowScene { + return foregroundScene + } + +- // If no valid foreground scene is found, return nil + return nil + } + +@@ -48,12 +102,7 @@ public class StoreReviewModule: Module { + return false + #endif + +- // For apps distributed through TestFlight or intalled from Xcode the receipt file is named "StoreKit/sandboxReceipt" +- // instead of "StoreKit/receipt" + let isSandboxEnv = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" +- +- // Apps distributed through TestFlight or the App Store will not have an embedded provisioning profile +- // Source: https://developer.apple.com/documentation/technotes/tn3125-inside-code-signing-provisioning-profiles#Profile-location + return isSandboxEnv && !hasEmbeddedMobileProvision() + } + +diff --git a/src/ExpoStoreReview.ts b/src/ExpoStoreReview.ts +index cb9ee95..94b9581 100644 +--- a/src/ExpoStoreReview.ts ++++ b/src/ExpoStoreReview.ts +@@ -2,4 +2,7 @@ + export default {} as Partial<{ + isAvailableAsync: () => Promise; + requestReview: () => Promise; ++ prePromptReview: () => Promise; ++ resetReviewState: () => Promise; ++ hasUserRated: () => Promise; + }>; +diff --git a/src/StoreReview.ts b/src/StoreReview.ts +index a9b9094..570caa6 100644 +--- a/src/StoreReview.ts ++++ b/src/StoreReview.ts +@@ -17,6 +17,20 @@ export async function isAvailableAsync(): Promise { + return StoreReview.isAvailableAsync?.() ?? false; + } + ++// @needsAudit ++/** ++ * Shows a pre-prompt alert asking the user if they'd like to rate the app. If they select "Yes", ++ * it will then show the native store review prompt. This is useful for improving review conversion rates. ++ * Currently only available on iOS. ++ */ ++export async function prePromptReview(): Promise { ++ if (StoreReview?.prePromptReview) { ++ return StoreReview.prePromptReview(); ++ } ++ // Fallback: if prePromptReview is not available, just do nothing ++ // (This will be the case on web and Android) ++} ++ + // @needsAudit + /** + * In ideal circumstances this will open a native modal and allow the user to select a star rating +@@ -78,3 +92,33 @@ export function storeUrl(): string | null { + export async function hasAction(): Promise { + return !!storeUrl() || (await isAvailableAsync()); + } ++ ++// @needsAudit ++/** ++ * Resets the review state stored in UserDefaults. This allows you to clear the ++ * tracking of whether the user has already been prompted for a review. ++ * Currently only available on iOS. ++ */ ++export async function resetReviewState(): Promise { ++ if (StoreReview?.resetReviewState) { ++ return StoreReview.resetReviewState(); ++ } ++ // Fallback: if resetReviewState is not available, just do nothing ++ // (This will be the case on web and Android) ++} ++ ++// @needsAudit ++/** ++ * Checks whether the user has already rated the app (i.e., they selected "Rate Now" ++ * in the pre-prompt dialog). This can be used to conditionally show or hide rating prompts. ++ * Currently only available on iOS. ++ * @return A promise that resolves to true if the user has rated, false otherwise. ++ */ ++export async function hasUserRated(): Promise { ++ if (StoreReview?.hasUserRated) { ++ return StoreReview.hasUserRated(); ++ } ++ // Fallback: if hasUserRated is not available, return false ++ // (This will be the case on web and Android) ++ return false; ++} diff --git a/.yarn/patches/react-native+0.81.4.patch b/.yarn/patches/react-native+0.81.4.patch new file mode 100644 index 0000000..1e023b2 --- /dev/null +++ b/.yarn/patches/react-native+0.81.4.patch @@ -0,0 +1,30 @@ +diff --git a/index.js b/index.js +index c737c93..f3edd25 100644 +--- a/index.js ++++ b/index.js +@@ -336,6 +336,9 @@ module.exports = { + return require('./src/private/components/virtualview/VirtualView') + .VirtualViewMode; + }, ++ get Slider() { ++ return require('@react-native-community/slider').default; ++ }, + // #endregion + } as ReactNativePublicAPI; + +@@ -405,15 +408,4 @@ if (__DEV__) { + * attempting to access Slider. */ + /* $FlowFixMe[invalid-export] This is intentional: Flow will error when + * attempting to access Slider. */ +- Object.defineProperty(module.exports, 'Slider', { +- configurable: true, +- get() { +- invariant( +- false, +- 'Slider has been removed from react-native core. ' + +- "It can now be installed and imported from '@react-native-community/slider' instead of 'react-native'. " + +- 'See https://github.com/callstack/react-native-slider', +- ); +- }, +- }); + } diff --git a/.yarn/patches/react-native-purchases+9.6.1.patch b/.yarn/patches/react-native-purchases+9.6.1.patch new file mode 100644 index 0000000..837fced --- /dev/null +++ b/.yarn/patches/react-native-purchases+9.6.1.patch @@ -0,0 +1,17 @@ +diff --git a/dist/utils/environment.js b/dist/utils/environment.js +index 7c5d453..8457e4b 100644 +--- a/dist/utils/environment.js ++++ b/dist/utils/environment.js +@@ -31,6 +31,12 @@ exports.shouldUseBrowserMode = shouldUseBrowserMode; + * Detects if the app is running in Expo Go + */ + function isExpoGo() { ++ if (!__DEV__) { ++ return false; ++ } ++ if (globalThis.expo && globalThis.expo.modules && globalThis.expo.modules.AnythingLauncherModule) { ++ return true; ++ } + var _a, _b; + if (!!react_native_1.NativeModules.RNPurchases) { + return false; diff --git a/.yarn/patches/react-native-purchases-ui+9.6.1.patch b/.yarn/patches/react-native-purchases-ui+9.6.1.patch new file mode 100644 index 0000000..f0df96f --- /dev/null +++ b/.yarn/patches/react-native-purchases-ui+9.6.1.patch @@ -0,0 +1,59 @@ +diff --git a/lib/commonjs/utils/environment.js b/lib/commonjs/utils/environment.js +index 43e5e6a..b67d36a 100644 +--- a/lib/commonjs/utils/environment.js ++++ b/lib/commonjs/utils/environment.js +@@ -31,6 +31,12 @@ function shouldUsePreviewAPIMode() { + */ + function isExpoGo() { + var _globalThis$expo; ++ if (!__DEV__) { ++ return false; ++ } ++ if (globalThis.expo && globalThis.expo.modules && globalThis.expo.modules.AnythingLauncherModule) { ++ return true; ++ } + if (!!_reactNative.NativeModules.RNPaywalls && !!_reactNative.NativeModules.RNCustomerCenter) { + return false; + } +diff --git a/lib/module/utils/environment.js b/lib/module/utils/environment.js +index 435d456..4002fe2 100644 +--- a/lib/module/utils/environment.js ++++ b/lib/module/utils/environment.js +@@ -26,6 +26,12 @@ export function shouldUsePreviewAPIMode() { + */ + function isExpoGo() { + var _globalThis$expo; ++ if (!__DEV__) { ++ return false; ++ } ++ if (globalThis.expo && globalThis.expo.modules && globalThis.expo.modules.AnythingLauncherModule) { ++ return true; ++ } + if (!!NativeModules.RNPaywalls && !!NativeModules.RNCustomerCenter) { + return false; + } +diff --git a/src/utils/environment.ts b/src/utils/environment.ts +index 5605bf2..ed86595 100644 +--- a/src/utils/environment.ts ++++ b/src/utils/environment.ts +@@ -26,6 +26,7 @@ declare global { + var expo: { + modules?: { + ExpoGo?: boolean; ++ AnythingLauncherModule?: boolean; + }; + }; + } +@@ -34,6 +35,12 @@ declare global { + * Detects if the app is running in Expo Go + */ + function isExpoGo(): boolean { ++ if (!__DEV__) { ++ return false; ++ } ++ if (globalThis.expo && globalThis.expo.modules && globalThis.expo.modules.AnythingLauncherModule) { ++ return true; ++ } + if (!!NativeModules.RNPaywalls && !!NativeModules.RNCustomerCenter) { + return false; + } diff --git a/.yarn/patches/react-native-web-refresh-control+1.1.2.patch b/.yarn/patches/react-native-web-refresh-control+1.1.2.patch new file mode 100644 index 0000000..81fb30b --- /dev/null +++ b/.yarn/patches/react-native-web-refresh-control+1.1.2.patch @@ -0,0 +1,24 @@ +diff --git a/src/RefreshControl.web.js b/src/RefreshControl.web.js +index b2351e6..c638d23 100644 +--- a/src/RefreshControl.web.js ++++ b/src/RefreshControl.web.js +@@ -1,5 +1,5 @@ + import React, { useRef, useEffect, useCallback, useMemo } from 'react' +-import { View, Text, PanResponder, Animated, ActivityIndicator, findNodeHandle } from 'react-native' ++import { View, Text, PanResponder, Animated, ActivityIndicator } from 'react-native' + import PropTypes from 'prop-types' + + const arrowIcon = +@@ -77,9 +77,9 @@ export default function RefreshControl({ + onStartShouldSetPanResponderCapture: () => false, + onMoveShouldSetPanResponder: (_,gestureState) => { + if (!containerRef.current) return false +- const containerDOM = findNodeHandle(containerRef.current) +- if (!containerDOM) return false +- return containerDOM.children[0].scrollTop === 0 ++ const scrollContainer = containerRef.current?.firstChild ++ if (!scrollContainer) return false ++ return scrollContainer.scrollTop === 0 + && (Math.abs(gestureState.dy) > Math.abs(gestureState.dx) * 2 && Math.abs(gestureState.vy) > Math.abs(gestureState.vx) * 2.5) + }, + onMoveShouldSetPanResponderCapture: () => false, diff --git a/.yarn/patches/sonner-native+0.21.0.patch b/.yarn/patches/sonner-native+0.21.0.patch new file mode 100644 index 0000000..f088284 --- /dev/null +++ b/.yarn/patches/sonner-native+0.21.0.patch @@ -0,0 +1,43 @@ +diff --git a/lib/commonjs/positioner.js b/lib/commonjs/positioner.js +index cac0f68..ec816b7 100644 +--- a/lib/commonjs/positioner.js ++++ b/lib/commonjs/positioner.js +@@ -55,8 +55,12 @@ const Positioner = ({ + return {}; + }, [position, bottom, top, offset]); + return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { ++ ...(_reactNative.Platform.OS === 'web' ? { ++ style: [{ pointerEvents: 'box-none' }, containerStyle, insetValues, style], ++ } :{ + style: [containerStyle, insetValues, style], + pointerEvents: "box-none", ++ }), + ...props, + children: children + }); +diff --git a/lib/module/positioner.js b/lib/module/positioner.js +index 476f6bb..40f1968 100644 +--- a/lib/module/positioner.js ++++ b/lib/module/positioner.js +@@ -1,7 +1,7 @@ + "use strict"; + + import React from 'react'; +-import { View } from 'react-native'; ++import { View, Platform } from 'react-native'; + import { useSafeAreaInsets } from 'react-native-safe-area-context'; + import { useToastContext } from "./context.js"; + import { jsx as _jsx } from "react/jsx-runtime"; +@@ -50,8 +50,12 @@ export const Positioner = ({ + return {}; + }, [position, bottom, top, offset]); + return /*#__PURE__*/_jsx(View, { ++...(Platform.OS === 'web' ? { ++ style: [{ pointerEvents: 'box-none' }, containerStyle, insetValues, style], ++ } :{ + style: [containerStyle, insetValues, style], + pointerEvents: "box-none", ++ }), + ...props, + children: children + }); diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..e317132 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,16 @@ +nodeLinker: node-modules +nmMode: hardlinks-global + +# These peer dep warnings are from upstream packages with stale ranges that +# work fine with our versions. @lshay/ui declares react 18 / tailwindcss 3 +# but works with 19 / 4; expo-three uses old expo SDK ranges; +# @expo/cli still requests the deprecated @types/react-native. +logFilters: + - code: YN0060 + level: discard + - code: YN0002 + level: discard + - code: YN0068 + level: discard + - code: YN0086 + level: discard diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fc438fe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,51 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +A cross-platform (iOS / Android / web) **time-tracking app for insole (orthotics) production**, built on the **"Create" / Anything AI** platform and exported to run locally. The UI is in **Dutch**. The user picks an insole type (`Type zool`: Kurk / Berk / 3D), a handling/task (`Type handeling`), and a count (`Aantal zolen`, default 2), then runs a stopwatch (start / pause / stop & save / double-press discard). History is exportable to CSV (nl-BE locale, `HH:MM:SS` durations); the Settings tab manages handelingen per zooltype. + +**This repo is frontend-only.** The backend API routes (`/api/tasks`, `/api/logs`) and the database (`production_tasks`, `time_logs` tables) live on the remote Create web app, reached via `EXPO_PUBLIC_BASE_URL` (see `apps/mobile/.env`). Editing tasks/history happens against that remote DB, not in this code. + +## Layout + +- Yarn 4 (Berry) monorepo, `node-modules` linker. Workspaces = `apps/*`; only **`apps/mobile`** (the Expo app) exists. +- **`publisher/`** is NOT a workspace — it's a standalone OpenNext + AWS S3 tool (its own `yarn.lock`) for building/deploying the Next.js *web* side. Rarely touched. + +## Commands + +Run from the repo root unless noted. There are no `start`/`lint`/`test` npm scripts — invoke the tools directly. + +```bash +yarn install # install (Yarn 4) +npx oxlint # lint (config: .oxlintrc.json) — this is the real linter +npx oxfmt # format (config: .oxfmtrc.json) — 2-space, single-quote, semi, width 100 + +cd apps/mobile +npx expo start # dev server; press a/i/w or scan the QR with Expo Go +npx expo start --web # web target only +npx tsc --noEmit # typecheck (strict; @/* -> src/*) +yarn jest # all tests (jest-expo preset) +yarn jest src/utils/iap/__tests__/useInAppPurchase.test.ts # single file +yarn jest -t "name" # single test by name + +eas build --profile --platform # native builds (eas.json) +``` + +> `eslint` / `typescript-eslint` are in devDeps but there is **no eslint config** in the tree — use **oxlint**, not eslint. + +## Architecture (the parts that span files) + +- **Routing:** Expo Router, file-based under `apps/mobile/src/app/`. `_layout.tsx` is the root: it gates render on `useAuth().initiate()` + `isReady` (loads the persisted session before showing anything) and provides the React Query client. `(tabs)/` holds the three screens — `index.tsx` (Stopwatch), `history.tsx` (Geschiedenis), `tasks.tsx` (Instellingen). +- **Platform vs web entry points are split by file extension.** Native: `index.tsx` → `entrypoint.ts` → `App.tsx`. Web: `index.web.tsx` → `App.web.tsx`. The `.web.*` files add sandbox-iframe plumbing (postMessage handshake, navigation sync, screenshot capture, healthcheck) used by the Create preview panel — this is why the web root looks very different from the native one. +- **Global `fetch` is monkey-patched.** `src/__create/polyfills.ts` replaces `global.fetch` with `src/__create/fetch.ts`, which rewrites first-party (`/...`) URLs onto `EXPO_PUBLIC_BASE_URL`, injects project/host headers, and attaches the SecureStore JWT. App code calls `fetch('/api/...')` and relies on this. For explicit auth, `src/utils/auth/getSession.ts` exposes `authFetch` (better-auth bearer JWT). +- **Web support is achieved via Metro module aliasing**, not separate web code. `apps/mobile/metro.config.js` `resolveRequest` swaps a large set of native modules for stubs in `polyfills/web/` when `platform === 'web'` (and a few in `polyfills/native/`, plus dev-only stubs like `react-native-purchases` outside production). **Consequence: adding a native dependency that gets imported on web requires adding a web polyfill alias here, or the web build breaks.** +- **Styling:** NativeWind/Tailwind config extends `@anythingai/app/tailwind.config`, but the screens themselves mostly use React Native `StyleSheet`/inline styles. Inter is the font. +- **Environment gating:** `EXPO_PUBLIC_CREATE_ENV` (`PRODUCTION` / `DEVELOPMENT`) and `__DEV__` gate analytics, Sentry, the in-app "anything-menu", and dev-only native aliases. Real native SDKs only load in production builds. + +## Conventions & gotchas + +- **Do not edit platform-managed files.** Files under `apps/mobile/__create/` and `apps/mobile/src/__create/`, and the auth files in `src/utils/auth/` (`useAuth.ts`, `getSession.ts`, etc.), carry `⚠ ANYTHING PLATFORM — DO NOT REWRITE` headers. They define the public auth surface (`signIn`/`signUp`/`signOut`/`auth`/`isAuthenticated`/`isReady`) and the fetch/sandbox plumbing; rewriting them breaks auth or the Create preview. `src/app/_layout.tsx` is editable except for the `` render and the `initiate()` + `isReady` gate. +- **Do not bump patched dependencies.** `react-native`, `expo-router`, `expo-store-review`, `react-native-purchases(-ui)`, `@expo/cli`, `@react-native-community/netinfo`, and others use `patch:` entries in `package.json` backed by `.yarn/patches/`, pinned further by root `resolutions`/`overrides`. Upgrading them discards the patch and breaks the app. When running `npx expo install --fix`, skip any package marked `patch:`. +- This is an exported template: package versions are pinned exact (no `^`) deliberately. Prefer minimal, targeted changes. diff --git a/apps/mobile/.easignore b/apps/mobile/.easignore new file mode 100644 index 0000000..ab35621 --- /dev/null +++ b/apps/mobile/.easignore @@ -0,0 +1,38 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# typescript +*.tsbuildinfo + +app-example + +caches/ +public/ \ No newline at end of file diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 0000000..316a8c5 --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,44 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +app-example + +.env + +.metro-virtual/* + +caches/* \ No newline at end of file diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx new file mode 100644 index 0000000..a6fda90 --- /dev/null +++ b/apps/mobile/App.tsx @@ -0,0 +1,16 @@ +import { App } from 'expo-router/build/qualified-entry'; +import { ScreenViewTracker } from './src/__create/analytics'; + +// Screen-view analytics is mounted here, in the entry, rather than in +// app/_layout. The entry is platform scaffold that ships with the template, so +// the tracker reaches every app on its next rebuild WITHOUT editing each +// project's own _layout (mirrors how App.web.tsx tracks navigation at the +// root). usePathname reads expo-router's global store. +export default function MobileRoot() { + return ( + <> + + + + ); +} diff --git a/apps/mobile/App.web.tsx b/apps/mobile/App.web.tsx new file mode 100644 index 0000000..8281330 --- /dev/null +++ b/apps/mobile/App.web.tsx @@ -0,0 +1,175 @@ +import { usePathname, useRouter } from 'expo-router'; +import { App } from 'expo-router/build/qualified-entry'; +import React, { memo, useEffect } from 'react'; +import './src/__create/polyfills'; + +import { ErrorBoundary } from './src/__create/ErrorBoundary'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { Toaster } from 'sonner-native'; +import { AlertModal } from './polyfills/web/alerts.web'; +import './global.css'; + +const RUNTIME_ERROR_PATTERNS = [ + /fetch failed/i, + /networks*(error|request)/i, + /failed to fetch/i, + /load failed/i, + /ECONNREFUSED/i, + /ECONNRESET/i, + /ETIMEDOUT/i, + /ENOTFOUND/i, + /ERR_CONNECTION/i, + /aborted/i, + /timeout/i, + /socket hang up/i, + /503\b/, + /502\b/, + /504\b/, + /getaddrinfo/i, +]; + +function isRuntimeError(msg: string) { + return RUNTIME_ERROR_PATTERNS.some((p) => p.test(msg)); +} + +function postErrorToParent(message: string, name: string, stack: string) { + try { + if (window.parent !== window) { + window.parent.postMessage( + { + type: 'sandbox:error:detected', + error: { message, name, stack }, + }, + '*' + ); + } + } catch {} +} + +const GlobalErrorReporter = () => { + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + const errorHandler = (event: ErrorEvent) => { + if (typeof event.preventDefault === 'function') event.preventDefault(); + console.error(event.error); + + const error = event.error; + const message = error?.message || event.message || 'Unknown error'; + if (!isRuntimeError(message)) { + postErrorToParent(message, error?.name || 'Error', error?.stack || ''); + } + }; + const unhandledRejectionHandler = (event: PromiseRejectionEvent) => { + if (typeof event.preventDefault === 'function') event.preventDefault(); + const reason = event.reason; + console.error('Unhandled promise rejection:', reason); + + const message = reason?.message || String(reason || ''); + if (isRuntimeError(message)) return; + const isCodeError = + reason instanceof TypeError || + reason instanceof ReferenceError || + reason instanceof SyntaxError || + reason?.code === 'MODULE_RESOLVE_FAILED'; + if (!isCodeError) return; + postErrorToParent(message, reason?.name || 'Error', reason?.stack || ''); + }; + window.addEventListener('error', errorHandler); + window.addEventListener('unhandledrejection', unhandledRejectionHandler); + return () => { + window.removeEventListener('error', errorHandler); + window.removeEventListener( + 'unhandledrejection', + unhandledRejectionHandler + ); + }; + }, []); + return null; +}; + +const Wrapper = memo(() => { + return ( + + + + + + + + ); +}); +const healthyResponse = { + type: 'sandbox:mobile:healthcheck:response', + healthy: true, +}; + +const useHandshakeParent = () => { + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (event.data.type === 'sandbox:mobile:healthcheck') { + window.parent.postMessage(healthyResponse, '*'); + } + }; + window.addEventListener('message', handleMessage); + // Immediately respond to the parent window with a healthy response in + // case we missed the healthcheck message + window.parent.postMessage(healthyResponse, '*'); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, []); +}; + +const CreateApp = () => { + const router = useRouter(); + const pathname = usePathname(); + useHandshakeParent(); + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if ( + event.data.type === 'sandbox:navigation' && + event.data.pathname !== pathname + ) { + router.push(event.data.pathname); + } + }; + + window.addEventListener('message', handleMessage); + window.parent.postMessage({ type: 'sandbox:mobile:ready' }, '*'); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, [router, pathname]); + + useEffect(() => { + window.parent.postMessage( + { + type: 'sandbox:mobile:navigation', + pathname, + }, + '*' + ); + }, [pathname]); + + return ( + <> + + + + ); +}; + +export default CreateApp; diff --git a/apps/mobile/__create/handle-resolve-request-error.js b/apps/mobile/__create/handle-resolve-request-error.js new file mode 100644 index 0000000..c54c200 --- /dev/null +++ b/apps/mobile/__create/handle-resolve-request-error.js @@ -0,0 +1,79 @@ +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const { reportErrorToRemote } = require('./report-error-to-remote'); + +const VIRTUAL_ROOT = path.join(__dirname, '../.metro-virtual'); +const VIRTUAL_ROOT_UNRESOLVED = path.join(VIRTUAL_ROOT, 'unresolved'); + +const handleResolveRequestError = ({ + error, + context, + moduleName, + platform, +}) => { + const errorMessage = `Unable to resolve module '${moduleName}' from '${context.originModulePath}'`; + const syntheticError = new Error(errorMessage); + syntheticError.stack = error.stack; + reportErrorToRemote({ error: syntheticError }).catch((_reportError) => { + // no-op + }); + if (process.env.NODE_ENV === 'production') throw error; + if (platform === 'android') throw error; + if (!__DEV__ && process.env.EXPO_PUBLIC_CREATE_ENV !== 'DEVELOPMENT') + throw error; + + // Build a deterministic virtual file path for this failed request + const key = `${moduleName}|${context.originModulePath}|${platform}`; + const hash = crypto + .createHash('sha256') + .update(key) + .digest('hex') + .slice(0, 16); + + fs.mkdirSync(VIRTUAL_ROOT_UNRESOLVED, { recursive: true }); + const vfile = path.join(VIRTUAL_ROOT_UNRESOLVED, `throw-${hash}.js`); + + // Serialize a safe payload for the client + const payload = { + moduleName, + from: context.originModulePath, + platform, + originalMessage: String( + error?.message ? error.message : 'Unknown resolve error' + ), + }; + + const code = [ + '// Auto generated by custom Metro resolver', + '(function(){', + ` var info = ${JSON.stringify(payload)};`, + " var msg = 'Unable to resolve \"' + info.moduleName + '\" from \"' + info.from + '\"';", + " msg += '\\n\\n' + info.originalMessage;", + ' var e = new Error(msg);', + " e.name = 'ModuleResolveError';", + " e.code = 'MODULE_RESOLVE_FAILED';", + ' throw e;', + '})();', + 'export {};', // keep ESM shape harmlessly + '', + ].join('\n'); + + // Only write if content changed — avoids bumping mtime and triggering Metro rebuild loop + const existingContent = fs.existsSync(vfile) ? fs.readFileSync(vfile, 'utf8') : null; + if (existingContent !== code) { + fs.writeFileSync(vfile, code, 'utf8'); + } + + // Tell Metro to load our thrower as a real source file + return { + filePath: vfile, + type: 'sourceFile', + }; +}; + +module.exports = { + handleResolveRequestError, + VIRTUAL_ROOT, + VIRTUAL_ROOT_UNRESOLVED, +}; diff --git a/apps/mobile/__create/report-error-to-remote.js b/apps/mobile/__create/report-error-to-remote.js new file mode 100644 index 0000000..76f444f --- /dev/null +++ b/apps/mobile/__create/report-error-to-remote.js @@ -0,0 +1,53 @@ +import { serializeError } from "serialize-error"; + +export const sendLogsToRemote = async (logs) => { + if ( + !process.env.EXPO_PUBLIC_LOGS_ENDPOINT || + !process.env.EXPO_PUBLIC_PROJECT_GROUP_ID || + !process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY + ) { + return { success: false }; + } + try { + const response = await fetch(process.env.EXPO_PUBLIC_LOGS_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY}`, + }, + body: JSON.stringify({ + projectGroupId: process.env.EXPO_PUBLIC_PROJECT_GROUP_ID, + logs, + }), + }); + if (!response.ok) { + return { success: false }; + } + } catch (fetchError) { + return { success: false, error: fetchError }; + } + return { success: true }; +}; + +export const reportErrorToRemote = async ({ error }) => { + if ( + !process.env.EXPO_PUBLIC_LOGS_ENDPOINT || + !process.env.EXPO_PUBLIC_PROJECT_GROUP_ID || + !process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY + ) { + console.debug( + "reportErrorToRemote: Missing environment variables for logging endpoint, project group ID, or API key.", + error, + ); + return { success: false }; + } + return sendLogsToRemote([ + { + message: JSON.stringify(serializeError(error)), + timestamp: new Date().toISOString(), + level: "error", + source: "BUILDER", + devServerId: process.env.EXPO_PUBLIC_DEV_SERVER_ID, + }, + ]); +}; diff --git a/apps/mobile/__create/report-error-to-remote.test.js b/apps/mobile/__create/report-error-to-remote.test.js new file mode 100644 index 0000000..82f18b0 --- /dev/null +++ b/apps/mobile/__create/report-error-to-remote.test.js @@ -0,0 +1,115 @@ +jest.mock("serialize-error", () => ({ + serializeError: jest.fn((err) => ({ + message: err instanceof Error ? err.message : String(err), + name: err instanceof Error ? err.name : "Error", + })), +})); + +let sendLogsToRemote; +let reportErrorToRemote; + +beforeEach(() => { + jest.resetAllMocks(); + jest.resetModules(); + delete process.env.EXPO_PUBLIC_LOGS_ENDPOINT; + delete process.env.EXPO_PUBLIC_PROJECT_GROUP_ID; + delete process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY; + delete process.env.EXPO_PUBLIC_DEV_SERVER_ID; + global.fetch = jest.fn(); + + // Re-require after mocks are set up + jest.doMock("serialize-error", () => ({ + serializeError: jest.fn((err) => ({ + message: err instanceof Error ? err.message : String(err), + name: err instanceof Error ? err.name : "Error", + })), + })); + const mod = require("./report-error-to-remote"); + sendLogsToRemote = mod.sendLogsToRemote; + reportErrorToRemote = mod.reportErrorToRemote; +}); + +describe("sendLogsToRemote", () => { + it("returns success: false when env vars are missing", async () => { + const result = await sendLogsToRemote([{ message: "test" }]); + expect(result).toEqual({ success: false }); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("sends logs to the endpoint with correct auth header", async () => { + process.env.EXPO_PUBLIC_LOGS_ENDPOINT = "https://logs.test/ingest"; + process.env.EXPO_PUBLIC_PROJECT_GROUP_ID = "pg-123"; + process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY = "key-abc"; + + global.fetch = jest.fn().mockResolvedValue({ ok: true }); + + const logs = [ + { message: "hello", level: "info", timestamp: "2026-01-01T00:00:00Z" }, + ]; + const result = await sendLogsToRemote(logs); + + expect(result).toEqual({ success: true }); + expect(global.fetch).toHaveBeenCalledWith("https://logs.test/ingest", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer key-abc", + }, + body: JSON.stringify({ projectGroupId: "pg-123", logs }), + }); + }); + + it("returns success: false on non-ok response", async () => { + process.env.EXPO_PUBLIC_LOGS_ENDPOINT = "https://logs.test/ingest"; + process.env.EXPO_PUBLIC_PROJECT_GROUP_ID = "pg-123"; + process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY = "key-abc"; + + global.fetch = jest.fn().mockResolvedValue({ ok: false, status: 500 }); + + const result = await sendLogsToRemote([{ message: "fail" }]); + expect(result).toEqual({ success: false }); + }); + + it("returns success: false with error on network failure", async () => { + process.env.EXPO_PUBLIC_LOGS_ENDPOINT = "https://logs.test/ingest"; + process.env.EXPO_PUBLIC_PROJECT_GROUP_ID = "pg-123"; + process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY = "key-abc"; + + const networkError = new Error("Network request failed"); + global.fetch = jest.fn().mockRejectedValue(networkError); + + const result = await sendLogsToRemote([{ message: "fail" }]); + expect(result).toEqual({ success: false, error: networkError }); + }); +}); + +describe("reportErrorToRemote", () => { + it("returns success: false when env vars are missing", async () => { + const result = await reportErrorToRemote({ + error: new Error("test error"), + }); + expect(result).toEqual({ success: false }); + expect(global.fetch).not.toHaveBeenCalled(); + }); + + it("serializes error and sends as a single log entry with source BUILDER", async () => { + process.env.EXPO_PUBLIC_LOGS_ENDPOINT = "https://logs.test/ingest"; + process.env.EXPO_PUBLIC_PROJECT_GROUP_ID = "pg-123"; + process.env.EXPO_PUBLIC_CREATE_TEMP_API_KEY = "key-abc"; + process.env.EXPO_PUBLIC_DEV_SERVER_ID = "ds-456"; + + global.fetch = jest.fn().mockResolvedValue({ ok: true }); + + const error = new Error("something broke"); + await reportErrorToRemote({ error }); + + expect(global.fetch).toHaveBeenCalledTimes(1); + const body = JSON.parse(global.fetch.mock.calls[0][1].body); + expect(body.projectGroupId).toBe("pg-123"); + expect(body.logs).toHaveLength(1); + expect(body.logs[0].level).toBe("error"); + expect(body.logs[0].source).toBe("BUILDER"); + expect(body.logs[0].devServerId).toBe("ds-456"); + expect(body.logs[0].message).toContain("something broke"); + }); +}); diff --git a/apps/mobile/__create/sentry.ts b/apps/mobile/__create/sentry.ts new file mode 100644 index 0000000..ee6a948 --- /dev/null +++ b/apps/mobile/__create/sentry.ts @@ -0,0 +1,78 @@ +import * as Sentry from "@sentry/react-native"; +import { sendLogsToRemote } from "./report-error-to-remote"; + +function isActive(): boolean { + return ( + !__DEV__ && + process.env.EXPO_PUBLIC_CREATE_ENV !== "DEVELOPMENT" && + !!process.env.EXPO_PUBLIC_SENTRY_DSN + ); +} + +let initialized = false; + +// Mirror a Sentry event into the Anything logs pipeline so native and JS +// crashes — including startup crashes that Sentry caches natively and reports +// on the next launch — surface in the Flux builder, not only the Sentry +// dashboard. +function forwardEventToRemote(event: Sentry.Event): void { + try { + const exception = event.exception?.values?.[0]; + const lines: string[] = []; + if (exception && (exception.type || exception.value)) { + lines.push(`${exception.type ?? "Error"}: ${exception.value ?? ""}`); + } else if (typeof event.message === "string") { + lines.push(event.message); + } + const frames = exception?.stacktrace?.frames; + if (frames && frames.length > 0) { + lines.push( + frames + .slice(-20) + .reverse() + .map( + (frame) => + ` at ${frame.function ?? "?"} (${frame.filename ?? "?"}:${frame.lineno ?? 0})`, + ) + .join("\n"), + ); + } + const message = lines.join("\n").trim(); + if (!message) return; + const timestamp = + typeof event.timestamp === "number" + ? new Date(event.timestamp * 1000).toISOString() + : new Date().toISOString(); + sendLogsToRemote([ + { + message: `[SENTRY] ${message}`, + timestamp, + level: "error", + source: "TEST_FLIGHT", + }, + ]); + } catch (_err) { + // Silent + } +} + +export function initSentry(): void { + try { + if (!isActive() || initialized) return; + initialized = true; + Sentry.init({ + dsn: process.env.EXPO_PUBLIC_SENTRY_DSN, + enableNativeCrashHandling: true, + beforeSend: (event) => { + forwardEventToRemote(event); + return event; + }, + }); + const projectGroupId = process.env.EXPO_PUBLIC_PROJECT_GROUP_ID; + if (projectGroupId) { + Sentry.setTag("projectGroupId", projectGroupId); + } + } catch (_err) { + // Silent — Sentry must never crash the host app + } +} diff --git a/apps/mobile/__create/testflight-logger.test.ts b/apps/mobile/__create/testflight-logger.test.ts new file mode 100644 index 0000000..079b4bb --- /dev/null +++ b/apps/mobile/__create/testflight-logger.test.ts @@ -0,0 +1,428 @@ +let mockSendLogsToRemote: jest.Mock; +let mockGetItem: jest.Mock; +let mockSetItem: jest.Mock; +let mockRemoveItem: jest.Mock; +let mockFileStore: Record; +let capturedErrorHandler: ((error: Error, isFatal?: boolean) => void) | null; +let originalErrorUtils: unknown; + +const CRASH_FILE = "/doc/testflight_crash_logs.json"; + +const STORAGE_KEY = "testflight_logger_pending_logs"; +let originalDev: boolean; + +function setDevMode(value: boolean) { + (globalThis as Record).__DEV__ = value; +} + +beforeEach(() => { + originalDev = (globalThis as Record).__DEV__ as boolean; + jest.resetModules(); + jest.useFakeTimers(); + process.env.EXPO_PUBLIC_CREATE_ENV = "PRODUCTION"; + + mockSendLogsToRemote = jest.fn().mockResolvedValue({ success: true }); + mockGetItem = jest.fn().mockResolvedValue(null); + mockSetItem = jest.fn().mockResolvedValue(undefined); + mockRemoveItem = jest.fn().mockResolvedValue(undefined); + mockFileStore = {}; + + capturedErrorHandler = null; + originalErrorUtils = (globalThis as Record).ErrorUtils; + (globalThis as Record).ErrorUtils = { + getGlobalHandler: () => () => {}, + setGlobalHandler: (handler: (error: Error, isFatal?: boolean) => void) => { + capturedErrorHandler = handler; + }, + }; + + jest.doMock("./report-error-to-remote", () => ({ + sendLogsToRemote: mockSendLogsToRemote, + })); + + jest.doMock("@react-native-async-storage/async-storage", () => ({ + getItem: mockGetItem, + setItem: mockSetItem, + removeItem: mockRemoveItem, + })); + + jest.doMock("expo-file-system", () => { + class MockFile { + uri: string; + constructor(directory: string, name: string) { + this.uri = `${directory}/${name}`; + } + get exists() { + return Object.prototype.hasOwnProperty.call(mockFileStore, this.uri); + } + create() { + if (!(this.uri in mockFileStore)) mockFileStore[this.uri] = ""; + } + delete() { + delete mockFileStore[this.uri]; + } + write(content: string) { + mockFileStore[this.uri] = content; + } + textSync() { + return mockFileStore[this.uri] ?? ""; + } + } + return { File: MockFile, Paths: { document: "/doc" } }; + }); +}); + +afterEach(() => { + jest.useRealTimers(); + setDevMode(originalDev); + delete process.env.EXPO_PUBLIC_CREATE_ENV; + (globalThis as Record).ErrorUtils = originalErrorUtils; +}); + +function loadModule() { + return require("./testflight-logger") as typeof import("./testflight-logger"); +} + +describe("initTestFlightLogger", () => { + it("is a no-op when __DEV__ is true", async () => { + setDevMode(true); + const { initTestFlightLogger, getTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + + expect(getTestFlightLogger()).toBeNull(); + expect(mockSendLogsToRemote).not.toHaveBeenCalled(); + }); + + it("is a no-op when EXPO_PUBLIC_CREATE_ENV is DEVELOPMENT", async () => { + setDevMode(false); + process.env.EXPO_PUBLIC_CREATE_ENV = "DEVELOPMENT"; + const { initTestFlightLogger, getTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + + expect(getTestFlightLogger()).toBeNull(); + }); + + it("activates when not in dev mode", async () => { + setDevMode(false); + const { initTestFlightLogger, getTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + const logger = getTestFlightLogger(); + expect(logger).not.toBeNull(); + expect(logger).toHaveProperty("logError"); + }); + + it("only creates one instance on multiple calls", async () => { + setDevMode(false); + const { initTestFlightLogger, getTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + const first = getTestFlightLogger(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + const second = getTestFlightLogger(); + + expect(first).toBe(second); + }); +}); + +describe("console patching", () => { + it("intercepts console.log and buffers the message", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + console.log("test message"); + + await jest.advanceTimersByTimeAsync(5_000); + + expect(mockSendLogsToRemote).toHaveBeenCalled(); + const logs = mockSendLogsToRemote.mock.calls[0][0]; + const logMessage = logs.find( + (l: Record) => l.message === "test message", + ); + expect(logMessage).toBeDefined(); + expect(logMessage.level).toBe("log"); + expect(logMessage.source).toBe("TEST_FLIGHT"); + }); + + it("intercepts console.error", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + console.error("bad thing"); + + await jest.advanceTimersByTimeAsync(5_000); + + expect(mockSendLogsToRemote).toHaveBeenCalled(); + const logs = mockSendLogsToRemote.mock.calls[0][0]; + const errorLog = logs.find( + (l: Record) => l.message === "bad thing", + ); + expect(errorLog).toBeDefined(); + expect(errorLog.level).toBe("error"); + }); + + it("serializes non-string arguments as JSON", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + console.log("count:", { x: 1 }); + + await jest.advanceTimersByTimeAsync(5_000); + + const logs = mockSendLogsToRemote.mock.calls[0][0]; + const entry = logs.find((l: Record) => + l.message.includes("count:"), + ); + expect(entry.message).toBe('count: {"x":1}'); + }); +}); + +describe("logError", () => { + it("immediately flushes error entries", async () => { + setDevMode(false); + const { initTestFlightLogger, getTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + const logger = getTestFlightLogger()!; + logger.logError("critical failure"); + + // Should flush without waiting for the 5s interval + await jest.advanceTimersByTimeAsync(0); + + expect(mockSendLogsToRemote).toHaveBeenCalled(); + const logs = mockSendLogsToRemote.mock.calls[0][0]; + expect(logs[0].message).toBe("critical failure"); + expect(logs[0].level).toBe("error"); + }); +}); + +describe("buffering and flushing", () => { + it("flushes buffer every 5 seconds", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + console.log("entry 1"); + + // Not flushed yet at 3 seconds + await jest.advanceTimersByTimeAsync(3_000); + expect(mockSendLogsToRemote).not.toHaveBeenCalled(); + + // Flushed at 5 seconds + await jest.advanceTimersByTimeAsync(2_000); + expect(mockSendLogsToRemote).toHaveBeenCalledTimes(1); + }); + + it("auto-flushes when buffer reaches 50 entries", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + for (let i = 0; i < 50; i++) { + console.log(`entry ${i}`); + } + + await jest.advanceTimersByTimeAsync(0); + expect(mockSendLogsToRemote).toHaveBeenCalled(); + }); +}); + +describe("persistence and retry", () => { + it("persists logs to AsyncStorage when flush fails", async () => { + mockSendLogsToRemote.mockResolvedValue({ success: false }); + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + console.log("will fail"); + + await jest.advanceTimersByTimeAsync(5_000); + + expect(mockSetItem).toHaveBeenCalledWith( + STORAGE_KEY, + expect.stringContaining("will fail"), + ); + }); + + it("restores persisted logs on startup and re-sends them", async () => { + const persistedLogs = [ + { + message: "old log", + timestamp: "2026-01-01T00:00:00Z", + level: "error", + source: "TEST_FLIGHT", + sessionId: "old-session", + }, + ]; + mockGetItem.mockResolvedValueOnce(JSON.stringify(persistedLogs)); + + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + expect(mockRemoveItem).toHaveBeenCalledWith(STORAGE_KEY); + expect(mockSendLogsToRemote).toHaveBeenCalledWith(persistedLogs); + }); + + it("re-persists restored logs if resend also fails", async () => { + const persistedLogs = [ + { + message: "stubborn log", + timestamp: "2026-01-01T00:00:00Z", + level: "error", + source: "TEST_FLIGHT", + sessionId: "old-session", + }, + ]; + mockGetItem + .mockResolvedValueOnce(JSON.stringify(persistedLogs)) + .mockResolvedValueOnce(null); + + mockSendLogsToRemote.mockResolvedValueOnce({ success: false }); + + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + expect(mockSetItem).toHaveBeenCalledWith( + STORAGE_KEY, + expect.stringContaining("stubborn log"), + ); + }); + + it("caps persisted entries at 200", async () => { + const existingLogs = Array.from({ length: 195 }, (_, i) => ({ + message: `existing ${i}`, + timestamp: "2026-01-01T00:00:00Z", + level: "log", + source: "TEST_FLIGHT", + sessionId: "s", + })); + + mockSendLogsToRemote.mockResolvedValue({ success: false }); + mockGetItem + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(JSON.stringify(existingLogs)); + + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + for (let i = 0; i < 10; i++) { + console.log(`new ${i}`); + } + + await jest.advanceTimersByTimeAsync(5_000); + + const setItemCalls = mockSetItem.mock.calls; + const lastCall = setItemCalls[setItemCalls.length - 1]; + const saved = JSON.parse(lastCall[1]); + expect(saved.length).toBeLessThanOrEqual(200); + }); +}); + +describe("crash persistence", () => { + it("synchronously snapshots the buffer to the crash file on a fatal error", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + console.log("breadcrumb before crash"); + capturedErrorHandler!(new Error("startup boom"), true); + + const crashFile = mockFileStore[CRASH_FILE]; + expect(crashFile).toContain("startup boom"); + expect(crashFile).toContain("breadcrumb before crash"); + }); + + it("does not snapshot the crash file for a non-fatal error", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + capturedErrorHandler!(new Error("recoverable"), false); + + expect(mockFileStore[CRASH_FILE]).toBeUndefined(); + }); + + it("captures the in-flight batch when a fatal error triggers auto-flush", async () => { + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + // One short of the auto-flush threshold; the fatal error is the 50th + // entry, so addEntry's auto-flush empties `buffer` before + // persistBufferSync runs. + for (let i = 0; i < 49; i++) { + console.log(`entry ${i}`); + } + capturedErrorHandler!(new Error("boundary crash"), true); + + const crashFile = mockFileStore[CRASH_FILE]; + expect(crashFile).toContain("boundary crash"); + expect(crashFile).toContain("entry 0"); + }); + + it("ships crash-file logs on the next startup and clears the file", async () => { + mockFileStore[CRASH_FILE] = JSON.stringify([ + { + message: "[FATAL] crashed last run", + timestamp: "2026-01-01T00:00:00Z", + level: "error", + source: "TEST_FLIGHT", + sessionId: "prev", + }, + ]); + setDevMode(false); + const { initTestFlightLogger } = loadModule(); + + initTestFlightLogger(); + await jest.advanceTimersByTimeAsync(0); + + expect(mockSendLogsToRemote).toHaveBeenCalled(); + const sent = mockSendLogsToRemote.mock.calls[0][0]; + expect( + sent.some( + (entry: Record) => + entry.message === "[FATAL] crashed last run", + ), + ).toBe(true); + expect(mockFileStore[CRASH_FILE]).toBeUndefined(); + }); +}); diff --git a/apps/mobile/__create/testflight-logger.ts b/apps/mobile/__create/testflight-logger.ts new file mode 100644 index 0000000..6ec8285 --- /dev/null +++ b/apps/mobile/__create/testflight-logger.ts @@ -0,0 +1,292 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { File, Paths } from "expo-file-system"; +import { AppState, type AppStateStatus } from "react-native"; +import { sendLogsToRemote } from "./report-error-to-remote"; + +const STORAGE_KEY = "testflight_logger_pending_logs"; +// Written synchronously from the crash handlers so logs survive a startup +// crash that tears down the JS runtime before the async network / AsyncStorage +// paths can finish. Shipped and cleared on the next launch. +const CRASH_FILE_NAME = "testflight_crash_logs.json"; +const MAX_STORED_ENTRIES = 200; +const MAX_BUFFER_SIZE = 50; +const FLUSH_INTERVAL_MS = 5_000; + +interface LogEntry { + message: string; + timestamp: string; + level: "log" | "info" | "warn" | "error" | "debug"; + source: "TEST_FLIGHT"; + sessionId: string; +} + +function isActive(): boolean { + return !__DEV__ && process.env.EXPO_PUBLIC_CREATE_ENV !== "DEVELOPMENT"; +} + +function generateSessionId(): string { + return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; +} + +let instance: TestFlightLogger | null = null; + +class TestFlightLogger { + private buffer: LogEntry[] = []; + // Entries spliced out of `buffer` by an in-flight `flush()` that hasn't + // confirmed delivery yet. Tracked so a crash mid-flush can still snapshot + // them — `buffer` alone would miss them. + private inFlightBatch: LogEntry[] = []; + private sessionId: string; + private flushTimer: ReturnType | null = null; + private originalConsole: Record void> = {}; + private isFlushing = false; + + constructor() { + this.sessionId = generateSessionId(); + } + + async start(): Promise { + try { + await this.restorePersistedLogs(); + this.patchConsole(); + this.hookUncaughtExceptions(); + this.hookUnhandledRejections(); + this.hookAppState(); + this.flushTimer = setInterval(() => { + this.flush(); + }, FLUSH_INTERVAL_MS); + } catch (_err) { + // Silent — the logger must never crash the host app + } + } + + logError(message: string): void { + try { + this.addEntry("error", message); + this.flush(); + } catch (_err) { + // Silent + } + } + + private addEntry(level: LogEntry["level"], message: string): void { + this.buffer.push({ + message, + timestamp: new Date().toISOString(), + level, + source: "TEST_FLIGHT", + sessionId: this.sessionId, + }); + if (this.buffer.length >= MAX_BUFFER_SIZE) { + this.flush(); + } + } + + private patchConsole(): void { + const levels = ["log", "info", "warn", "error", "debug"] as const; + for (const level of levels) { + this.originalConsole[level] = console[level].bind(console); + console[level] = (...args: unknown[]) => { + try { + const message = args + .map((arg) => { + if (typeof arg === "string") return arg; + try { + return JSON.stringify(arg); + } catch { + return String(arg); + } + }) + .join(" "); + this.addEntry(level, message); + } catch (_err) { + // Silent + } + this.originalConsole[level]?.(...args); + }; + } + } + + private hookUncaughtExceptions(): void { + const ErrorUtils = (globalThis as Record).ErrorUtils as + | { + getGlobalHandler: () => (error: Error, isFatal?: boolean) => void; + setGlobalHandler: ( + handler: (error: Error, isFatal?: boolean) => void, + ) => void; + } + | undefined; + + if (!ErrorUtils) return; + + const previousHandler = ErrorUtils.getGlobalHandler(); + ErrorUtils.setGlobalHandler((error: Error, isFatal?: boolean) => { + try { + const tag = isFatal ? "[FATAL]" : "[UNCAUGHT]"; + this.addEntry("error", `${tag} ${error.message}\n${error.stack ?? ""}`); + // Only a fatal error tears down the runtime before the async + // flush can finish, so only then is the synchronous crash-file + // snapshot needed. Non-fatal errors are delivered by flush / + // AsyncStorage retry; snapshotting them would just duplicate + // entries on the next launch. + if (isFatal) { + this.persistBufferSync(); + } + this.flush(); + } catch (_err) { + // Silent + } + previousHandler(error, isFatal); + }); + } + + private hookUnhandledRejections(): void { + const previous: ((event: PromiseRejectionEvent) => void) | null = + globalThis.onunhandledrejection; + globalThis.onunhandledrejection = (event: PromiseRejectionEvent) => { + try { + const reason = + event.reason instanceof Error + ? `${event.reason.message}\n${event.reason.stack ?? ""}` + : String(event.reason); + this.addEntry("error", `[UNHANDLED_REJECTION] ${reason}`); + this.flush(); + } catch (_err) { + // Silent + } + if (previous) { + previous(event); + } + }; + } + + private hookAppState(): void { + AppState.addEventListener("change", (state: AppStateStatus) => { + try { + this.addEntry("info", `[APP_STATE] ${state}`); + if (state === "background" || state === "inactive") { + this.flush(); + } + } catch (_err) { + // Silent + } + }); + } + + private async flush(): Promise { + if (this.isFlushing || this.buffer.length === 0) return; + this.isFlushing = true; + const batch = this.buffer.splice(0); + this.inFlightBatch = batch; + try { + const result = await sendLogsToRemote(batch); + if (!result.success) { + await this.persistLogs(batch); + } + } catch (_err) { + await this.persistLogs(batch); + } finally { + this.inFlightBatch = []; + this.isFlushing = false; + } + } + + private async persistLogs(logs: LogEntry[]): Promise { + try { + const raw = await AsyncStorage.getItem(STORAGE_KEY); + const existing: LogEntry[] = raw ? JSON.parse(raw) : []; + const merged = [...existing, ...logs].slice(-MAX_STORED_ENTRIES); + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(merged)); + } catch (_err) { + // Silent + } + } + + // Synchronously snapshot pending logs to disk. Called from the crash + // handlers before the runtime is torn down, so a startup crash is still + // recoverable on the next launch. Includes any in-flight flush batch, + // since `addEntry`'s auto-flush may have already emptied `buffer`. + private persistBufferSync(): void { + try { + const pending = [...this.inFlightBatch, ...this.buffer]; + if (pending.length === 0) return; + const merged = [...this.readCrashLogsSync(), ...pending].slice( + -MAX_STORED_ENTRIES, + ); + const file = new File(Paths.document, CRASH_FILE_NAME); + if (file.exists) { + file.delete(); + } + file.create(); + file.write(JSON.stringify(merged)); + } catch (_err) { + // Silent — the logger must never crash the host app + } + } + + private readCrashLogsSync(): LogEntry[] { + try { + const file = new File(Paths.document, CRASH_FILE_NAME); + if (!file.exists) return []; + const parsed: unknown = JSON.parse(file.textSync()); + return Array.isArray(parsed) ? (parsed as LogEntry[]) : []; + } catch (_err) { + return []; + } + } + + private clearCrashFileSync(): void { + try { + const file = new File(Paths.document, CRASH_FILE_NAME); + if (file.exists) { + file.delete(); + } + } catch (_err) { + // Silent + } + } + + private async restorePersistedLogs(): Promise { + try { + const crashLogs = this.readCrashLogsSync(); + if (crashLogs.length > 0) { + this.clearCrashFileSync(); + } + const raw = await AsyncStorage.getItem(STORAGE_KEY); + if (raw) { + await AsyncStorage.removeItem(STORAGE_KEY); + } + const storedLogs: LogEntry[] = raw ? JSON.parse(raw) : []; + const logs = [...crashLogs, ...storedLogs]; + if (logs.length === 0) return; + const result = await sendLogsToRemote(logs); + if (!result.success) { + await this.persistLogs(logs); + } + } catch (_err) { + // Silent + } + } +} + +export function initTestFlightLogger(): void { + try { + if (!isActive()) return; + if (instance) return; + instance = new TestFlightLogger(); + instance.start(); + } catch (_err) { + // Silent + } +} + +export function getTestFlightLogger(): { + logError: (message: string) => void; +} | null { + try { + if (!isActive()) return null; + return instance; + } catch (_err) { + return null; + } +} diff --git a/apps/mobile/app.json b/apps/mobile/app.json new file mode 100644 index 0000000..eb70818 --- /dev/null +++ b/apps/mobile/app.json @@ -0,0 +1,82 @@ +{ + "expo": { + "name": "Anything mobile app", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true, + "infoPlist": { + "ITSAppUsesNonExemptEncryption": false + } + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/images/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "permissions": [ + "android.permission.RECORD_AUDIO", + "android.permission.MODIFY_AUDIO_SETTINGS" + ], + "package": "xyz.create.CreateExpoEnvironment" + }, + "plugins": [ + [ + "expo-router", + { + "sitemap": false + } + ], + [ + "expo-splash-screen", + { + "image": "./assets/images/splash-icon.png", + "imageWidth": 200, + "resizeMode": "contain" + } + ], + "expo-audio", + [ + "expo-build-properties", + { + "ios": { + "useFrameworks": "static" + } + } + ], + [ + "expo-video", + { + "supportsBackgroundPlayback": true, + "supportsPictureInPicture": true + } + ], + "expo-font", + "expo-secure-store", + "expo-web-browser", + "@sentry/react-native/expo", + [ + "react-native-google-mobile-ads", + { + "androidAppId": "ca-app-pub-3940256099942544~3347511713", + "iosAppId": "ca-app-pub-3940256099942544~1458002511" + } + ] + ], + "web": { + "bundler": "metro", + "favicon": "./assets/images/favicon.png" + }, + "experiments": { + "typedRoutes": true + }, + "extra": { + "router": { + "origin": false + } + } + } +} \ No newline at end of file diff --git a/apps/mobile/assets/images/adaptive-icon.png b/apps/mobile/assets/images/adaptive-icon.png new file mode 100644 index 0000000..eba6b34 Binary files /dev/null and b/apps/mobile/assets/images/adaptive-icon.png differ diff --git a/apps/mobile/assets/images/favicon.png b/apps/mobile/assets/images/favicon.png new file mode 100644 index 0000000..6e8eb9c Binary files /dev/null and b/apps/mobile/assets/images/favicon.png differ diff --git a/apps/mobile/assets/images/icon.png b/apps/mobile/assets/images/icon.png new file mode 100644 index 0000000..917bc11 Binary files /dev/null and b/apps/mobile/assets/images/icon.png differ diff --git a/apps/mobile/assets/images/splash-icon.png b/apps/mobile/assets/images/splash-icon.png new file mode 100644 index 0000000..eba6b34 Binary files /dev/null and b/apps/mobile/assets/images/splash-icon.png differ diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js new file mode 100644 index 0000000..04bc056 --- /dev/null +++ b/apps/mobile/babel.config.js @@ -0,0 +1,6 @@ +module.exports = (api) => { + api.cache(true); + return { + presets: [['babel-preset-expo', { unstable_transformImportMeta: true }]], + }; +}; diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json new file mode 100644 index 0000000..f959b91 --- /dev/null +++ b/apps/mobile/eas.json @@ -0,0 +1,40 @@ +{ + "cli": { + "version": ">= 15.0.15", + "appVersionSource": "remote" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal", + "env": { + "SENTRY_DISABLE_AUTO_UPLOAD": "true" + } + }, + "preview": { + "distribution": "internal", + "env": { + "SENTRY_DISABLE_AUTO_UPLOAD": "true" + } + }, + "production": { + "autoIncrement": true, + "android": { + "buildType": "app-bundle" + }, + "env": { + "SENTRY_DISABLE_AUTO_UPLOAD": "true" + } + } + }, + "submit": { + "production": { + "android": { + "serviceAccountKeyPath": "./google-service-account.json", + "track": "internal", + "releaseStatus": "draft", + "changesNotSentForReview": false + } + } + } +} diff --git a/apps/mobile/entrypoint.ts b/apps/mobile/entrypoint.ts new file mode 100644 index 0000000..f1f2a24 --- /dev/null +++ b/apps/mobile/entrypoint.ts @@ -0,0 +1,3 @@ +import App from './App'; + +export default App; diff --git a/apps/mobile/fontawesome.css b/apps/mobile/fontawesome.css new file mode 100644 index 0000000..251d40a --- /dev/null +++ b/apps/mobile/fontawesome.css @@ -0,0 +1,8320 @@ +/*! + * Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + * Copyright 2024 Fonticons, Inc. + */ +.fa { + font-family: var(--fa-style-family, "FontAwesome"); + font-weight: var(--fa-style, 900); +} + +.fas, +.far, +.fab, +.fa-solid, +.fa-regular, +.fa-brands, +.fa { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: var(--fa-display, inline-block); + font-style: normal; + font-variant: normal; + line-height: 1; + text-rendering: auto; +} + +.fas::before, +.far::before, +.fab::before, +.fa-solid::before, +.fa-regular::before, +.fa-brands::before, +.fa::before { + content: var(--fa); +} + +.fa-classic, +.fas, +.fa-solid, +.far, +.fa-regular { + font-family: "FontAwesome"; +} + +.fa-brands, +.fab { + font-family: "FontAwesome"; +} + +.fa-1x { + font-size: 1em; +} + +.fa-2x { + font-size: 2em; +} + +.fa-3x { + font-size: 3em; +} + +.fa-4x { + font-size: 4em; +} + +.fa-5x { + font-size: 5em; +} + +.fa-6x { + font-size: 6em; +} + +.fa-7x { + font-size: 7em; +} + +.fa-8x { + font-size: 8em; +} + +.fa-9x { + font-size: 9em; +} + +.fa-10x { + font-size: 10em; +} + +.fa-2xs { + font-size: 0.625em; + line-height: 0.1em; + vertical-align: 0.225em; +} + +.fa-xs { + font-size: 0.75em; + line-height: 0.08333em; + vertical-align: 0.125em; +} + +.fa-sm { + font-size: 0.875em; + line-height: 0.07143em; + vertical-align: 0.05357em; +} + +.fa-lg { + font-size: 1.25em; + line-height: 0.05em; + vertical-align: -0.075em; +} + +.fa-xl { + font-size: 1.5em; + line-height: 0.04167em; + vertical-align: -0.125em; +} + +.fa-2xl { + font-size: 2em; + line-height: 0.03125em; + vertical-align: -0.1875em; +} + +.fa-fw { + text-align: center; + width: 1.25em; +} + +.fa-ul { + list-style-type: none; + margin-left: var(--fa-li-margin, 2.5em); + padding-left: 0; +} +.fa-ul > li { + position: relative; +} + +.fa-li { + left: calc(-1 * var(--fa-li-width, 2em)); + position: absolute; + text-align: center; + width: var(--fa-li-width, 2em); + line-height: inherit; +} + +.fa-border { + border-color: var(--fa-border-color, #eee); + border-radius: var(--fa-border-radius, 0.1em); + border-style: var(--fa-border-style, solid); + border-width: var(--fa-border-width, 0.08em); + padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); +} + +.fa-pull-left { + float: left; + margin-right: var(--fa-pull-margin, 0.3em); +} + +.fa-pull-right { + float: right; + margin-left: var(--fa-pull-margin, 0.3em); +} + +.fa-beat { + animation-name: fa-beat; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-bounce { + animation-name: fa-bounce; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var( + --fa-animation-timing, + cubic-bezier(0.28, 0.84, 0.42, 1) + ); +} + +.fa-fade { + animation-name: fa-fade; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var( + --fa-animation-timing, + cubic-bezier(0.4, 0, 0.6, 1) + ); +} + +.fa-beat-fade { + animation-name: fa-beat-fade; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var( + --fa-animation-timing, + cubic-bezier(0.4, 0, 0.6, 1) + ); +} + +.fa-flip { + animation-name: fa-flip; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, ease-in-out); +} + +.fa-shake { + animation-name: fa-shake; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin { + animation-name: fa-spin; + animation-delay: var(--fa-animation-delay, 0s); + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 2s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, linear); +} + +.fa-spin-reverse { + --fa-animation-direction: reverse; +} + +.fa-pulse, +.fa-spin-pulse { + animation-name: fa-spin; + animation-direction: var(--fa-animation-direction, normal); + animation-duration: var(--fa-animation-duration, 1s); + animation-iteration-count: var(--fa-animation-iteration-count, infinite); + animation-timing-function: var(--fa-animation-timing, steps(8)); +} + +@media (prefers-reduced-motion: reduce) { + .fa-beat, + .fa-bounce, + .fa-fade, + .fa-beat-fade, + .fa-flip, + .fa-pulse, + .fa-shake, + .fa-spin, + .fa-spin-pulse { + animation-delay: -1ms; + animation-duration: 1ms; + animation-iteration-count: 1; + transition-delay: 0s; + transition-duration: 0s; + } +} + +@keyframes fa-beat { + 0%, + 90% { + transform: scale(1); + } + 45% { + transform: scale(var(--fa-beat-scale, 1.25)); + } +} + +@keyframes fa-bounce { + 0% { + transform: scale(1, 1) translateY(0); + } + 10% { + transform: scale( + var(--fa-bounce-start-scale-x, 1.1), + var(--fa-bounce-start-scale-y, 0.9) + ) + translateY(0); + } + 30% { + transform: scale( + var(--fa-bounce-jump-scale-x, 0.9), + var(--fa-bounce-jump-scale-y, 1.1) + ) + translateY(var(--fa-bounce-height, -0.5em)); + } + 50% { + transform: scale( + var(--fa-bounce-land-scale-x, 1.05), + var(--fa-bounce-land-scale-y, 0.95) + ) + translateY(0); + } + 57% { + transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); + } + 64% { + transform: scale(1, 1) translateY(0); + } + 100% { + transform: scale(1, 1) translateY(0); + } +} + +@keyframes fa-fade { + 50% { + opacity: var(--fa-fade-opacity, 0.4); + } +} + +@keyframes fa-beat-fade { + 0%, + 100% { + opacity: var(--fa-beat-fade-opacity, 0.4); + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(var(--fa-beat-fade-scale, 1.125)); + } +} + +@keyframes fa-flip { + 50% { + transform: rotate3d( + var(--fa-flip-x, 0), + var(--fa-flip-y, 1), + var(--fa-flip-z, 0), + var(--fa-flip-angle, -180deg) + ); + } +} + +@keyframes fa-shake { + 0% { + transform: rotate(-15deg); + } + 4% { + transform: rotate(15deg); + } + 8%, + 24% { + transform: rotate(-18deg); + } + 12%, + 28% { + transform: rotate(18deg); + } + 16% { + transform: rotate(-22deg); + } + 20% { + transform: rotate(22deg); + } + 32% { + transform: rotate(-12deg); + } + 36% { + transform: rotate(12deg); + } + 40%, + 100% { + transform: rotate(0deg); + } +} + +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.fa-rotate-90 { + transform: rotate(90deg); +} + +.fa-rotate-180 { + transform: rotate(180deg); +} + +.fa-rotate-270 { + transform: rotate(270deg); +} + +.fa-flip-horizontal { + transform: scale(-1, 1); +} + +.fa-flip-vertical { + transform: scale(1, -1); +} + +.fa-flip-both, +.fa-flip-horizontal.fa-flip-vertical { + transform: scale(-1, -1); +} + +.fa-rotate-by { + transform: rotate(var(--fa-rotate-angle, 0)); +} + +.fa-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2.5em; +} + +.fa-stack-1x, +.fa-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; + z-index: var(--fa-stack-z-index, auto); +} + +.fa-stack-1x { + line-height: inherit; +} + +.fa-stack-2x { + font-size: 2em; +} + +.fa-inverse { + color: var(--fa-inverse, #fff); +} + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.fa-0 { + --fa: "\30"; +} + +.fa-1 { + --fa: "\31"; +} + +.fa-2 { + --fa: "\32"; +} + +.fa-3 { + --fa: "\33"; +} + +.fa-4 { + --fa: "\34"; +} + +.fa-5 { + --fa: "\35"; +} + +.fa-6 { + --fa: "\36"; +} + +.fa-7 { + --fa: "\37"; +} + +.fa-8 { + --fa: "\38"; +} + +.fa-9 { + --fa: "\39"; +} + +.fa-fill-drip { + --fa: "\f576"; +} + +.fa-arrows-to-circle { + --fa: "\e4bd"; +} + +.fa-circle-chevron-right { + --fa: "\f138"; +} + +.fa-chevron-circle-right { + --fa: "\f138"; +} + +.fa-at { + --fa: "\40"; +} + +.fa-trash-can { + --fa: "\f2ed"; +} + +.fa-trash-alt { + --fa: "\f2ed"; +} + +.fa-text-height { + --fa: "\f034"; +} + +.fa-user-xmark { + --fa: "\f235"; +} + +.fa-user-times { + --fa: "\f235"; +} + +.fa-stethoscope { + --fa: "\f0f1"; +} + +.fa-message { + --fa: "\f27a"; +} + +.fa-comment-alt { + --fa: "\f27a"; +} + +.fa-info { + --fa: "\f129"; +} + +.fa-down-left-and-up-right-to-center { + --fa: "\f422"; +} + +.fa-compress-alt { + --fa: "\f422"; +} + +.fa-explosion { + --fa: "\e4e9"; +} + +.fa-file-lines { + --fa: "\f15c"; +} + +.fa-file-alt { + --fa: "\f15c"; +} + +.fa-file-text { + --fa: "\f15c"; +} + +.fa-wave-square { + --fa: "\f83e"; +} + +.fa-ring { + --fa: "\f70b"; +} + +.fa-building-un { + --fa: "\e4d9"; +} + +.fa-dice-three { + --fa: "\f527"; +} + +.fa-calendar-days { + --fa: "\f073"; +} + +.fa-calendar-alt { + --fa: "\f073"; +} + +.fa-anchor-circle-check { + --fa: "\e4aa"; +} + +.fa-building-circle-arrow-right { + --fa: "\e4d1"; +} + +.fa-volleyball { + --fa: "\f45f"; +} + +.fa-volleyball-ball { + --fa: "\f45f"; +} + +.fa-arrows-up-to-line { + --fa: "\e4c2"; +} + +.fa-sort-down { + --fa: "\f0dd"; +} + +.fa-sort-desc { + --fa: "\f0dd"; +} + +.fa-circle-minus { + --fa: "\f056"; +} + +.fa-minus-circle { + --fa: "\f056"; +} + +.fa-door-open { + --fa: "\f52b"; +} + +.fa-right-from-bracket { + --fa: "\f2f5"; +} + +.fa-sign-out-alt { + --fa: "\f2f5"; +} + +.fa-atom { + --fa: "\f5d2"; +} + +.fa-soap { + --fa: "\e06e"; +} + +.fa-icons { + --fa: "\f86d"; +} + +.fa-heart-music-camera-bolt { + --fa: "\f86d"; +} + +.fa-microphone-lines-slash { + --fa: "\f539"; +} + +.fa-microphone-alt-slash { + --fa: "\f539"; +} + +.fa-bridge-circle-check { + --fa: "\e4c9"; +} + +.fa-pump-medical { + --fa: "\e06a"; +} + +.fa-fingerprint { + --fa: "\f577"; +} + +.fa-hand-point-right { + --fa: "\f0a4"; +} + +.fa-magnifying-glass-location { + --fa: "\f689"; +} + +.fa-search-location { + --fa: "\f689"; +} + +.fa-forward-step { + --fa: "\f051"; +} + +.fa-step-forward { + --fa: "\f051"; +} + +.fa-face-smile-beam { + --fa: "\f5b8"; +} + +.fa-smile-beam { + --fa: "\f5b8"; +} + +.fa-flag-checkered { + --fa: "\f11e"; +} + +.fa-football { + --fa: "\f44e"; +} + +.fa-football-ball { + --fa: "\f44e"; +} + +.fa-school-circle-exclamation { + --fa: "\e56c"; +} + +.fa-crop { + --fa: "\f125"; +} + +.fa-angles-down { + --fa: "\f103"; +} + +.fa-angle-double-down { + --fa: "\f103"; +} + +.fa-users-rectangle { + --fa: "\e594"; +} + +.fa-people-roof { + --fa: "\e537"; +} + +.fa-people-line { + --fa: "\e534"; +} + +.fa-beer-mug-empty { + --fa: "\f0fc"; +} + +.fa-beer { + --fa: "\f0fc"; +} + +.fa-diagram-predecessor { + --fa: "\e477"; +} + +.fa-arrow-up-long { + --fa: "\f176"; +} + +.fa-long-arrow-up { + --fa: "\f176"; +} + +.fa-fire-flame-simple { + --fa: "\f46a"; +} + +.fa-burn { + --fa: "\f46a"; +} + +.fa-person { + --fa: "\f183"; +} + +.fa-male { + --fa: "\f183"; +} + +.fa-laptop { + --fa: "\f109"; +} + +.fa-file-csv { + --fa: "\f6dd"; +} + +.fa-menorah { + --fa: "\f676"; +} + +.fa-truck-plane { + --fa: "\e58f"; +} + +.fa-record-vinyl { + --fa: "\f8d9"; +} + +.fa-face-grin-stars { + --fa: "\f587"; +} + +.fa-grin-stars { + --fa: "\f587"; +} + +.fa-bong { + --fa: "\f55c"; +} + +.fa-spaghetti-monster-flying { + --fa: "\f67b"; +} + +.fa-pastafarianism { + --fa: "\f67b"; +} + +.fa-arrow-down-up-across-line { + --fa: "\e4af"; +} + +.fa-spoon { + --fa: "\f2e5"; +} + +.fa-utensil-spoon { + --fa: "\f2e5"; +} + +.fa-jar-wheat { + --fa: "\e517"; +} + +.fa-envelopes-bulk { + --fa: "\f674"; +} + +.fa-mail-bulk { + --fa: "\f674"; +} + +.fa-file-circle-exclamation { + --fa: "\e4eb"; +} + +.fa-circle-h { + --fa: "\f47e"; +} + +.fa-hospital-symbol { + --fa: "\f47e"; +} + +.fa-pager { + --fa: "\f815"; +} + +.fa-address-book { + --fa: "\f2b9"; +} + +.fa-contact-book { + --fa: "\f2b9"; +} + +.fa-strikethrough { + --fa: "\f0cc"; +} + +.fa-k { + --fa: "\4b"; +} + +.fa-landmark-flag { + --fa: "\e51c"; +} + +.fa-pencil { + --fa: "\f303"; +} + +.fa-pencil-alt { + --fa: "\f303"; +} + +.fa-backward { + --fa: "\f04a"; +} + +.fa-caret-right { + --fa: "\f0da"; +} + +.fa-comments { + --fa: "\f086"; +} + +.fa-paste { + --fa: "\f0ea"; +} + +.fa-file-clipboard { + --fa: "\f0ea"; +} + +.fa-code-pull-request { + --fa: "\e13c"; +} + +.fa-clipboard-list { + --fa: "\f46d"; +} + +.fa-truck-ramp-box { + --fa: "\f4de"; +} + +.fa-truck-loading { + --fa: "\f4de"; +} + +.fa-user-check { + --fa: "\f4fc"; +} + +.fa-vial-virus { + --fa: "\e597"; +} + +.fa-sheet-plastic { + --fa: "\e571"; +} + +.fa-blog { + --fa: "\f781"; +} + +.fa-user-ninja { + --fa: "\f504"; +} + +.fa-person-arrow-up-from-line { + --fa: "\e539"; +} + +.fa-scroll-torah { + --fa: "\f6a0"; +} + +.fa-torah { + --fa: "\f6a0"; +} + +.fa-broom-ball { + --fa: "\f458"; +} + +.fa-quidditch { + --fa: "\f458"; +} + +.fa-quidditch-broom-ball { + --fa: "\f458"; +} + +.fa-toggle-off { + --fa: "\f204"; +} + +.fa-box-archive { + --fa: "\f187"; +} + +.fa-archive { + --fa: "\f187"; +} + +.fa-person-drowning { + --fa: "\e545"; +} + +.fa-arrow-down-9-1 { + --fa: "\f886"; +} + +.fa-sort-numeric-desc { + --fa: "\f886"; +} + +.fa-sort-numeric-down-alt { + --fa: "\f886"; +} + +.fa-face-grin-tongue-squint { + --fa: "\f58a"; +} + +.fa-grin-tongue-squint { + --fa: "\f58a"; +} + +.fa-spray-can { + --fa: "\f5bd"; +} + +.fa-truck-monster { + --fa: "\f63b"; +} + +.fa-w { + --fa: "\57"; +} + +.fa-earth-africa { + --fa: "\f57c"; +} + +.fa-globe-africa { + --fa: "\f57c"; +} + +.fa-rainbow { + --fa: "\f75b"; +} + +.fa-circle-notch { + --fa: "\f1ce"; +} + +.fa-tablet-screen-button { + --fa: "\f3fa"; +} + +.fa-tablet-alt { + --fa: "\f3fa"; +} + +.fa-paw { + --fa: "\f1b0"; +} + +.fa-cloud { + --fa: "\f0c2"; +} + +.fa-trowel-bricks { + --fa: "\e58a"; +} + +.fa-face-flushed { + --fa: "\f579"; +} + +.fa-flushed { + --fa: "\f579"; +} + +.fa-hospital-user { + --fa: "\f80d"; +} + +.fa-tent-arrow-left-right { + --fa: "\e57f"; +} + +.fa-gavel { + --fa: "\f0e3"; +} + +.fa-legal { + --fa: "\f0e3"; +} + +.fa-binoculars { + --fa: "\f1e5"; +} + +.fa-microphone-slash { + --fa: "\f131"; +} + +.fa-box-tissue { + --fa: "\e05b"; +} + +.fa-motorcycle { + --fa: "\f21c"; +} + +.fa-bell-concierge { + --fa: "\f562"; +} + +.fa-concierge-bell { + --fa: "\f562"; +} + +.fa-pen-ruler { + --fa: "\f5ae"; +} + +.fa-pencil-ruler { + --fa: "\f5ae"; +} + +.fa-people-arrows { + --fa: "\e068"; +} + +.fa-people-arrows-left-right { + --fa: "\e068"; +} + +.fa-mars-and-venus-burst { + --fa: "\e523"; +} + +.fa-square-caret-right { + --fa: "\f152"; +} + +.fa-caret-square-right { + --fa: "\f152"; +} + +.fa-scissors { + --fa: "\f0c4"; +} + +.fa-cut { + --fa: "\f0c4"; +} + +.fa-sun-plant-wilt { + --fa: "\e57a"; +} + +.fa-toilets-portable { + --fa: "\e584"; +} + +.fa-hockey-puck { + --fa: "\f453"; +} + +.fa-table { + --fa: "\f0ce"; +} + +.fa-magnifying-glass-arrow-right { + --fa: "\e521"; +} + +.fa-tachograph-digital { + --fa: "\f566"; +} + +.fa-digital-tachograph { + --fa: "\f566"; +} + +.fa-users-slash { + --fa: "\e073"; +} + +.fa-clover { + --fa: "\e139"; +} + +.fa-reply { + --fa: "\f3e5"; +} + +.fa-mail-reply { + --fa: "\f3e5"; +} + +.fa-star-and-crescent { + --fa: "\f699"; +} + +.fa-house-fire { + --fa: "\e50c"; +} + +.fa-square-minus { + --fa: "\f146"; +} + +.fa-minus-square { + --fa: "\f146"; +} + +.fa-helicopter { + --fa: "\f533"; +} + +.fa-compass { + --fa: "\f14e"; +} + +.fa-square-caret-down { + --fa: "\f150"; +} + +.fa-caret-square-down { + --fa: "\f150"; +} + +.fa-file-circle-question { + --fa: "\e4ef"; +} + +.fa-laptop-code { + --fa: "\f5fc"; +} + +.fa-swatchbook { + --fa: "\f5c3"; +} + +.fa-prescription-bottle { + --fa: "\f485"; +} + +.fa-bars { + --fa: "\f0c9"; +} + +.fa-navicon { + --fa: "\f0c9"; +} + +.fa-people-group { + --fa: "\e533"; +} + +.fa-hourglass-end { + --fa: "\f253"; +} + +.fa-hourglass-3 { + --fa: "\f253"; +} + +.fa-heart-crack { + --fa: "\f7a9"; +} + +.fa-heart-broken { + --fa: "\f7a9"; +} + +.fa-square-up-right { + --fa: "\f360"; +} + +.fa-external-link-square-alt { + --fa: "\f360"; +} + +.fa-face-kiss-beam { + --fa: "\f597"; +} + +.fa-kiss-beam { + --fa: "\f597"; +} + +.fa-film { + --fa: "\f008"; +} + +.fa-ruler-horizontal { + --fa: "\f547"; +} + +.fa-people-robbery { + --fa: "\e536"; +} + +.fa-lightbulb { + --fa: "\f0eb"; +} + +.fa-caret-left { + --fa: "\f0d9"; +} + +.fa-circle-exclamation { + --fa: "\f06a"; +} + +.fa-exclamation-circle { + --fa: "\f06a"; +} + +.fa-school-circle-xmark { + --fa: "\e56d"; +} + +.fa-arrow-right-from-bracket { + --fa: "\f08b"; +} + +.fa-sign-out { + --fa: "\f08b"; +} + +.fa-circle-chevron-down { + --fa: "\f13a"; +} + +.fa-chevron-circle-down { + --fa: "\f13a"; +} + +.fa-unlock-keyhole { + --fa: "\f13e"; +} + +.fa-unlock-alt { + --fa: "\f13e"; +} + +.fa-cloud-showers-heavy { + --fa: "\f740"; +} + +.fa-headphones-simple { + --fa: "\f58f"; +} + +.fa-headphones-alt { + --fa: "\f58f"; +} + +.fa-sitemap { + --fa: "\f0e8"; +} + +.fa-circle-dollar-to-slot { + --fa: "\f4b9"; +} + +.fa-donate { + --fa: "\f4b9"; +} + +.fa-memory { + --fa: "\f538"; +} + +.fa-road-spikes { + --fa: "\e568"; +} + +.fa-fire-burner { + --fa: "\e4f1"; +} + +.fa-flag { + --fa: "\f024"; +} + +.fa-hanukiah { + --fa: "\f6e6"; +} + +.fa-feather { + --fa: "\f52d"; +} + +.fa-volume-low { + --fa: "\f027"; +} + +.fa-volume-down { + --fa: "\f027"; +} + +.fa-comment-slash { + --fa: "\f4b3"; +} + +.fa-cloud-sun-rain { + --fa: "\f743"; +} + +.fa-compress { + --fa: "\f066"; +} + +.fa-wheat-awn { + --fa: "\e2cd"; +} + +.fa-wheat-alt { + --fa: "\e2cd"; +} + +.fa-ankh { + --fa: "\f644"; +} + +.fa-hands-holding-child { + --fa: "\e4fa"; +} + +.fa-asterisk { + --fa: "\2a"; +} + +.fa-square-check { + --fa: "\f14a"; +} + +.fa-check-square { + --fa: "\f14a"; +} + +.fa-peseta-sign { + --fa: "\e221"; +} + +.fa-heading { + --fa: "\f1dc"; +} + +.fa-header { + --fa: "\f1dc"; +} + +.fa-ghost { + --fa: "\f6e2"; +} + +.fa-list { + --fa: "\f03a"; +} + +.fa-list-squares { + --fa: "\f03a"; +} + +.fa-square-phone-flip { + --fa: "\f87b"; +} + +.fa-phone-square-alt { + --fa: "\f87b"; +} + +.fa-cart-plus { + --fa: "\f217"; +} + +.fa-gamepad { + --fa: "\f11b"; +} + +.fa-circle-dot { + --fa: "\f192"; +} + +.fa-dot-circle { + --fa: "\f192"; +} + +.fa-face-dizzy { + --fa: "\f567"; +} + +.fa-dizzy { + --fa: "\f567"; +} + +.fa-egg { + --fa: "\f7fb"; +} + +.fa-house-medical-circle-xmark { + --fa: "\e513"; +} + +.fa-campground { + --fa: "\f6bb"; +} + +.fa-folder-plus { + --fa: "\f65e"; +} + +.fa-futbol { + --fa: "\f1e3"; +} + +.fa-futbol-ball { + --fa: "\f1e3"; +} + +.fa-soccer-ball { + --fa: "\f1e3"; +} + +.fa-paintbrush { + --fa: "\f1fc"; +} + +.fa-paint-brush { + --fa: "\f1fc"; +} + +.fa-lock { + --fa: "\f023"; +} + +.fa-gas-pump { + --fa: "\f52f"; +} + +.fa-hot-tub-person { + --fa: "\f593"; +} + +.fa-hot-tub { + --fa: "\f593"; +} + +.fa-map-location { + --fa: "\f59f"; +} + +.fa-map-marked { + --fa: "\f59f"; +} + +.fa-house-flood-water { + --fa: "\e50e"; +} + +.fa-tree { + --fa: "\f1bb"; +} + +.fa-bridge-lock { + --fa: "\e4cc"; +} + +.fa-sack-dollar { + --fa: "\f81d"; +} + +.fa-pen-to-square { + --fa: "\f044"; +} + +.fa-edit { + --fa: "\f044"; +} + +.fa-car-side { + --fa: "\f5e4"; +} + +.fa-share-nodes { + --fa: "\f1e0"; +} + +.fa-share-alt { + --fa: "\f1e0"; +} + +.fa-heart-circle-minus { + --fa: "\e4ff"; +} + +.fa-hourglass-half { + --fa: "\f252"; +} + +.fa-hourglass-2 { + --fa: "\f252"; +} + +.fa-microscope { + --fa: "\f610"; +} + +.fa-sink { + --fa: "\e06d"; +} + +.fa-bag-shopping { + --fa: "\f290"; +} + +.fa-shopping-bag { + --fa: "\f290"; +} + +.fa-arrow-down-z-a { + --fa: "\f881"; +} + +.fa-sort-alpha-desc { + --fa: "\f881"; +} + +.fa-sort-alpha-down-alt { + --fa: "\f881"; +} + +.fa-mitten { + --fa: "\f7b5"; +} + +.fa-person-rays { + --fa: "\e54d"; +} + +.fa-users { + --fa: "\f0c0"; +} + +.fa-eye-slash { + --fa: "\f070"; +} + +.fa-flask-vial { + --fa: "\e4f3"; +} + +.fa-hand { + --fa: "\f256"; +} + +.fa-hand-paper { + --fa: "\f256"; +} + +.fa-om { + --fa: "\f679"; +} + +.fa-worm { + --fa: "\e599"; +} + +.fa-house-circle-xmark { + --fa: "\e50b"; +} + +.fa-plug { + --fa: "\f1e6"; +} + +.fa-chevron-up { + --fa: "\f077"; +} + +.fa-hand-spock { + --fa: "\f259"; +} + +.fa-stopwatch { + --fa: "\f2f2"; +} + +.fa-face-kiss { + --fa: "\f596"; +} + +.fa-kiss { + --fa: "\f596"; +} + +.fa-bridge-circle-xmark { + --fa: "\e4cb"; +} + +.fa-face-grin-tongue { + --fa: "\f589"; +} + +.fa-grin-tongue { + --fa: "\f589"; +} + +.fa-chess-bishop { + --fa: "\f43a"; +} + +.fa-face-grin-wink { + --fa: "\f58c"; +} + +.fa-grin-wink { + --fa: "\f58c"; +} + +.fa-ear-deaf { + --fa: "\f2a4"; +} + +.fa-deaf { + --fa: "\f2a4"; +} + +.fa-deafness { + --fa: "\f2a4"; +} + +.fa-hard-of-hearing { + --fa: "\f2a4"; +} + +.fa-road-circle-check { + --fa: "\e564"; +} + +.fa-dice-five { + --fa: "\f523"; +} + +.fa-square-rss { + --fa: "\f143"; +} + +.fa-rss-square { + --fa: "\f143"; +} + +.fa-land-mine-on { + --fa: "\e51b"; +} + +.fa-i-cursor { + --fa: "\f246"; +} + +.fa-stamp { + --fa: "\f5bf"; +} + +.fa-stairs { + --fa: "\e289"; +} + +.fa-i { + --fa: "\49"; +} + +.fa-hryvnia-sign { + --fa: "\f6f2"; +} + +.fa-hryvnia { + --fa: "\f6f2"; +} + +.fa-pills { + --fa: "\f484"; +} + +.fa-face-grin-wide { + --fa: "\f581"; +} + +.fa-grin-alt { + --fa: "\f581"; +} + +.fa-tooth { + --fa: "\f5c9"; +} + +.fa-v { + --fa: "\56"; +} + +.fa-bangladeshi-taka-sign { + --fa: "\e2e6"; +} + +.fa-bicycle { + --fa: "\f206"; +} + +.fa-staff-snake { + --fa: "\e579"; +} + +.fa-rod-asclepius { + --fa: "\e579"; +} + +.fa-rod-snake { + --fa: "\e579"; +} + +.fa-staff-aesculapius { + --fa: "\e579"; +} + +.fa-head-side-cough-slash { + --fa: "\e062"; +} + +.fa-truck-medical { + --fa: "\f0f9"; +} + +.fa-ambulance { + --fa: "\f0f9"; +} + +.fa-wheat-awn-circle-exclamation { + --fa: "\e598"; +} + +.fa-snowman { + --fa: "\f7d0"; +} + +.fa-mortar-pestle { + --fa: "\f5a7"; +} + +.fa-road-barrier { + --fa: "\e562"; +} + +.fa-school { + --fa: "\f549"; +} + +.fa-igloo { + --fa: "\f7ae"; +} + +.fa-joint { + --fa: "\f595"; +} + +.fa-angle-right { + --fa: "\f105"; +} + +.fa-horse { + --fa: "\f6f0"; +} + +.fa-q { + --fa: "\51"; +} + +.fa-g { + --fa: "\47"; +} + +.fa-notes-medical { + --fa: "\f481"; +} + +.fa-temperature-half { + --fa: "\f2c9"; +} + +.fa-temperature-2 { + --fa: "\f2c9"; +} + +.fa-thermometer-2 { + --fa: "\f2c9"; +} + +.fa-thermometer-half { + --fa: "\f2c9"; +} + +.fa-dong-sign { + --fa: "\e169"; +} + +.fa-capsules { + --fa: "\f46b"; +} + +.fa-poo-storm { + --fa: "\f75a"; +} + +.fa-poo-bolt { + --fa: "\f75a"; +} + +.fa-face-frown-open { + --fa: "\f57a"; +} + +.fa-frown-open { + --fa: "\f57a"; +} + +.fa-hand-point-up { + --fa: "\f0a6"; +} + +.fa-money-bill { + --fa: "\f0d6"; +} + +.fa-bookmark { + --fa: "\f02e"; +} + +.fa-align-justify { + --fa: "\f039"; +} + +.fa-umbrella-beach { + --fa: "\f5ca"; +} + +.fa-helmet-un { + --fa: "\e503"; +} + +.fa-bullseye { + --fa: "\f140"; +} + +.fa-bacon { + --fa: "\f7e5"; +} + +.fa-hand-point-down { + --fa: "\f0a7"; +} + +.fa-arrow-up-from-bracket { + --fa: "\e09a"; +} + +.fa-folder { + --fa: "\f07b"; +} + +.fa-folder-blank { + --fa: "\f07b"; +} + +.fa-file-waveform { + --fa: "\f478"; +} + +.fa-file-medical-alt { + --fa: "\f478"; +} + +.fa-radiation { + --fa: "\f7b9"; +} + +.fa-chart-simple { + --fa: "\e473"; +} + +.fa-mars-stroke { + --fa: "\f229"; +} + +.fa-vial { + --fa: "\f492"; +} + +.fa-gauge { + --fa: "\f624"; +} + +.fa-dashboard { + --fa: "\f624"; +} + +.fa-gauge-med { + --fa: "\f624"; +} + +.fa-tachometer-alt-average { + --fa: "\f624"; +} + +.fa-wand-magic-sparkles { + --fa: "\e2ca"; +} + +.fa-magic-wand-sparkles { + --fa: "\e2ca"; +} + +.fa-e { + --fa: "\45"; +} + +.fa-pen-clip { + --fa: "\f305"; +} + +.fa-pen-alt { + --fa: "\f305"; +} + +.fa-bridge-circle-exclamation { + --fa: "\e4ca"; +} + +.fa-user { + --fa: "\f007"; +} + +.fa-school-circle-check { + --fa: "\e56b"; +} + +.fa-dumpster { + --fa: "\f793"; +} + +.fa-van-shuttle { + --fa: "\f5b6"; +} + +.fa-shuttle-van { + --fa: "\f5b6"; +} + +.fa-building-user { + --fa: "\e4da"; +} + +.fa-square-caret-left { + --fa: "\f191"; +} + +.fa-caret-square-left { + --fa: "\f191"; +} + +.fa-highlighter { + --fa: "\f591"; +} + +.fa-key { + --fa: "\f084"; +} + +.fa-bullhorn { + --fa: "\f0a1"; +} + +.fa-globe { + --fa: "\f0ac"; +} + +.fa-synagogue { + --fa: "\f69b"; +} + +.fa-person-half-dress { + --fa: "\e548"; +} + +.fa-road-bridge { + --fa: "\e563"; +} + +.fa-location-arrow { + --fa: "\f124"; +} + +.fa-c { + --fa: "\43"; +} + +.fa-tablet-button { + --fa: "\f10a"; +} + +.fa-building-lock { + --fa: "\e4d6"; +} + +.fa-pizza-slice { + --fa: "\f818"; +} + +.fa-money-bill-wave { + --fa: "\f53a"; +} + +.fa-chart-area { + --fa: "\f1fe"; +} + +.fa-area-chart { + --fa: "\f1fe"; +} + +.fa-house-flag { + --fa: "\e50d"; +} + +.fa-person-circle-minus { + --fa: "\e540"; +} + +.fa-ban { + --fa: "\f05e"; +} + +.fa-cancel { + --fa: "\f05e"; +} + +.fa-camera-rotate { + --fa: "\e0d8"; +} + +.fa-spray-can-sparkles { + --fa: "\f5d0"; +} + +.fa-air-freshener { + --fa: "\f5d0"; +} + +.fa-star { + --fa: "\f005"; +} + +.fa-repeat { + --fa: "\f363"; +} + +.fa-cross { + --fa: "\f654"; +} + +.fa-box { + --fa: "\f466"; +} + +.fa-venus-mars { + --fa: "\f228"; +} + +.fa-arrow-pointer { + --fa: "\f245"; +} + +.fa-mouse-pointer { + --fa: "\f245"; +} + +.fa-maximize { + --fa: "\f31e"; +} + +.fa-expand-arrows-alt { + --fa: "\f31e"; +} + +.fa-charging-station { + --fa: "\f5e7"; +} + +.fa-shapes { + --fa: "\f61f"; +} + +.fa-triangle-circle-square { + --fa: "\f61f"; +} + +.fa-shuffle { + --fa: "\f074"; +} + +.fa-random { + --fa: "\f074"; +} + +.fa-person-running { + --fa: "\f70c"; +} + +.fa-running { + --fa: "\f70c"; +} + +.fa-mobile-retro { + --fa: "\e527"; +} + +.fa-grip-lines-vertical { + --fa: "\f7a5"; +} + +.fa-spider { + --fa: "\f717"; +} + +.fa-hands-bound { + --fa: "\e4f9"; +} + +.fa-file-invoice-dollar { + --fa: "\f571"; +} + +.fa-plane-circle-exclamation { + --fa: "\e556"; +} + +.fa-x-ray { + --fa: "\f497"; +} + +.fa-spell-check { + --fa: "\f891"; +} + +.fa-slash { + --fa: "\f715"; +} + +.fa-computer-mouse { + --fa: "\f8cc"; +} + +.fa-mouse { + --fa: "\f8cc"; +} + +.fa-arrow-right-to-bracket { + --fa: "\f090"; +} + +.fa-sign-in { + --fa: "\f090"; +} + +.fa-shop-slash { + --fa: "\e070"; +} + +.fa-store-alt-slash { + --fa: "\e070"; +} + +.fa-server { + --fa: "\f233"; +} + +.fa-virus-covid-slash { + --fa: "\e4a9"; +} + +.fa-shop-lock { + --fa: "\e4a5"; +} + +.fa-hourglass-start { + --fa: "\f251"; +} + +.fa-hourglass-1 { + --fa: "\f251"; +} + +.fa-blender-phone { + --fa: "\f6b6"; +} + +.fa-building-wheat { + --fa: "\e4db"; +} + +.fa-person-breastfeeding { + --fa: "\e53a"; +} + +.fa-right-to-bracket { + --fa: "\f2f6"; +} + +.fa-sign-in-alt { + --fa: "\f2f6"; +} + +.fa-venus { + --fa: "\f221"; +} + +.fa-passport { + --fa: "\f5ab"; +} + +.fa-thumbtack-slash { + --fa: "\e68f"; +} + +.fa-thumb-tack-slash { + --fa: "\e68f"; +} + +.fa-heart-pulse { + --fa: "\f21e"; +} + +.fa-heartbeat { + --fa: "\f21e"; +} + +.fa-people-carry-box { + --fa: "\f4ce"; +} + +.fa-people-carry { + --fa: "\f4ce"; +} + +.fa-temperature-high { + --fa: "\f769"; +} + +.fa-microchip { + --fa: "\f2db"; +} + +.fa-crown { + --fa: "\f521"; +} + +.fa-weight-hanging { + --fa: "\f5cd"; +} + +.fa-xmarks-lines { + --fa: "\e59a"; +} + +.fa-file-prescription { + --fa: "\f572"; +} + +.fa-weight-scale { + --fa: "\f496"; +} + +.fa-weight { + --fa: "\f496"; +} + +.fa-user-group { + --fa: "\f500"; +} + +.fa-user-friends { + --fa: "\f500"; +} + +.fa-arrow-up-a-z { + --fa: "\f15e"; +} + +.fa-sort-alpha-up { + --fa: "\f15e"; +} + +.fa-chess-knight { + --fa: "\f441"; +} + +.fa-face-laugh-squint { + --fa: "\f59b"; +} + +.fa-laugh-squint { + --fa: "\f59b"; +} + +.fa-wheelchair { + --fa: "\f193"; +} + +.fa-circle-arrow-up { + --fa: "\f0aa"; +} + +.fa-arrow-circle-up { + --fa: "\f0aa"; +} + +.fa-toggle-on { + --fa: "\f205"; +} + +.fa-person-walking { + --fa: "\f554"; +} + +.fa-walking { + --fa: "\f554"; +} + +.fa-l { + --fa: "\4c"; +} + +.fa-fire { + --fa: "\f06d"; +} + +.fa-bed-pulse { + --fa: "\f487"; +} + +.fa-procedures { + --fa: "\f487"; +} + +.fa-shuttle-space { + --fa: "\f197"; +} + +.fa-space-shuttle { + --fa: "\f197"; +} + +.fa-face-laugh { + --fa: "\f599"; +} + +.fa-laugh { + --fa: "\f599"; +} + +.fa-folder-open { + --fa: "\f07c"; +} + +.fa-heart-circle-plus { + --fa: "\e500"; +} + +.fa-code-fork { + --fa: "\e13b"; +} + +.fa-city { + --fa: "\f64f"; +} + +.fa-microphone-lines { + --fa: "\f3c9"; +} + +.fa-microphone-alt { + --fa: "\f3c9"; +} + +.fa-pepper-hot { + --fa: "\f816"; +} + +.fa-unlock { + --fa: "\f09c"; +} + +.fa-colon-sign { + --fa: "\e140"; +} + +.fa-headset { + --fa: "\f590"; +} + +.fa-store-slash { + --fa: "\e071"; +} + +.fa-road-circle-xmark { + --fa: "\e566"; +} + +.fa-user-minus { + --fa: "\f503"; +} + +.fa-mars-stroke-up { + --fa: "\f22a"; +} + +.fa-mars-stroke-v { + --fa: "\f22a"; +} + +.fa-champagne-glasses { + --fa: "\f79f"; +} + +.fa-glass-cheers { + --fa: "\f79f"; +} + +.fa-clipboard { + --fa: "\f328"; +} + +.fa-house-circle-exclamation { + --fa: "\e50a"; +} + +.fa-file-arrow-up { + --fa: "\f574"; +} + +.fa-file-upload { + --fa: "\f574"; +} + +.fa-wifi { + --fa: "\f1eb"; +} + +.fa-wifi-3 { + --fa: "\f1eb"; +} + +.fa-wifi-strong { + --fa: "\f1eb"; +} + +.fa-bath { + --fa: "\f2cd"; +} + +.fa-bathtub { + --fa: "\f2cd"; +} + +.fa-underline { + --fa: "\f0cd"; +} + +.fa-user-pen { + --fa: "\f4ff"; +} + +.fa-user-edit { + --fa: "\f4ff"; +} + +.fa-signature { + --fa: "\f5b7"; +} + +.fa-stroopwafel { + --fa: "\f551"; +} + +.fa-bold { + --fa: "\f032"; +} + +.fa-anchor-lock { + --fa: "\e4ad"; +} + +.fa-building-ngo { + --fa: "\e4d7"; +} + +.fa-manat-sign { + --fa: "\e1d5"; +} + +.fa-not-equal { + --fa: "\f53e"; +} + +.fa-border-top-left { + --fa: "\f853"; +} + +.fa-border-style { + --fa: "\f853"; +} + +.fa-map-location-dot { + --fa: "\f5a0"; +} + +.fa-map-marked-alt { + --fa: "\f5a0"; +} + +.fa-jedi { + --fa: "\f669"; +} + +.fa-square-poll-vertical { + --fa: "\f681"; +} + +.fa-poll { + --fa: "\f681"; +} + +.fa-mug-hot { + --fa: "\f7b6"; +} + +.fa-car-battery { + --fa: "\f5df"; +} + +.fa-battery-car { + --fa: "\f5df"; +} + +.fa-gift { + --fa: "\f06b"; +} + +.fa-dice-two { + --fa: "\f528"; +} + +.fa-chess-queen { + --fa: "\f445"; +} + +.fa-glasses { + --fa: "\f530"; +} + +.fa-chess-board { + --fa: "\f43c"; +} + +.fa-building-circle-check { + --fa: "\e4d2"; +} + +.fa-person-chalkboard { + --fa: "\e53d"; +} + +.fa-mars-stroke-right { + --fa: "\f22b"; +} + +.fa-mars-stroke-h { + --fa: "\f22b"; +} + +.fa-hand-back-fist { + --fa: "\f255"; +} + +.fa-hand-rock { + --fa: "\f255"; +} + +.fa-square-caret-up { + --fa: "\f151"; +} + +.fa-caret-square-up { + --fa: "\f151"; +} + +.fa-cloud-showers-water { + --fa: "\e4e4"; +} + +.fa-chart-bar { + --fa: "\f080"; +} + +.fa-bar-chart { + --fa: "\f080"; +} + +.fa-hands-bubbles { + --fa: "\e05e"; +} + +.fa-hands-wash { + --fa: "\e05e"; +} + +.fa-less-than-equal { + --fa: "\f537"; +} + +.fa-train { + --fa: "\f238"; +} + +.fa-eye-low-vision { + --fa: "\f2a8"; +} + +.fa-low-vision { + --fa: "\f2a8"; +} + +.fa-crow { + --fa: "\f520"; +} + +.fa-sailboat { + --fa: "\e445"; +} + +.fa-window-restore { + --fa: "\f2d2"; +} + +.fa-square-plus { + --fa: "\f0fe"; +} + +.fa-plus-square { + --fa: "\f0fe"; +} + +.fa-torii-gate { + --fa: "\f6a1"; +} + +.fa-frog { + --fa: "\f52e"; +} + +.fa-bucket { + --fa: "\e4cf"; +} + +.fa-image { + --fa: "\f03e"; +} + +.fa-microphone { + --fa: "\f130"; +} + +.fa-cow { + --fa: "\f6c8"; +} + +.fa-caret-up { + --fa: "\f0d8"; +} + +.fa-screwdriver { + --fa: "\f54a"; +} + +.fa-folder-closed { + --fa: "\e185"; +} + +.fa-house-tsunami { + --fa: "\e515"; +} + +.fa-square-nfi { + --fa: "\e576"; +} + +.fa-arrow-up-from-ground-water { + --fa: "\e4b5"; +} + +.fa-martini-glass { + --fa: "\f57b"; +} + +.fa-glass-martini-alt { + --fa: "\f57b"; +} + +.fa-square-binary { + --fa: "\e69b"; +} + +.fa-rotate-left { + --fa: "\f2ea"; +} + +.fa-rotate-back { + --fa: "\f2ea"; +} + +.fa-rotate-backward { + --fa: "\f2ea"; +} + +.fa-undo-alt { + --fa: "\f2ea"; +} + +.fa-table-columns { + --fa: "\f0db"; +} + +.fa-columns { + --fa: "\f0db"; +} + +.fa-lemon { + --fa: "\f094"; +} + +.fa-head-side-mask { + --fa: "\e063"; +} + +.fa-handshake { + --fa: "\f2b5"; +} + +.fa-gem { + --fa: "\f3a5"; +} + +.fa-dolly { + --fa: "\f472"; +} + +.fa-dolly-box { + --fa: "\f472"; +} + +.fa-smoking { + --fa: "\f48d"; +} + +.fa-minimize { + --fa: "\f78c"; +} + +.fa-compress-arrows-alt { + --fa: "\f78c"; +} + +.fa-monument { + --fa: "\f5a6"; +} + +.fa-snowplow { + --fa: "\f7d2"; +} + +.fa-angles-right { + --fa: "\f101"; +} + +.fa-angle-double-right { + --fa: "\f101"; +} + +.fa-cannabis { + --fa: "\f55f"; +} + +.fa-circle-play { + --fa: "\f144"; +} + +.fa-play-circle { + --fa: "\f144"; +} + +.fa-tablets { + --fa: "\f490"; +} + +.fa-ethernet { + --fa: "\f796"; +} + +.fa-euro-sign { + --fa: "\f153"; +} + +.fa-eur { + --fa: "\f153"; +} + +.fa-euro { + --fa: "\f153"; +} + +.fa-chair { + --fa: "\f6c0"; +} + +.fa-circle-check { + --fa: "\f058"; +} + +.fa-check-circle { + --fa: "\f058"; +} + +.fa-circle-stop { + --fa: "\f28d"; +} + +.fa-stop-circle { + --fa: "\f28d"; +} + +.fa-compass-drafting { + --fa: "\f568"; +} + +.fa-drafting-compass { + --fa: "\f568"; +} + +.fa-plate-wheat { + --fa: "\e55a"; +} + +.fa-icicles { + --fa: "\f7ad"; +} + +.fa-person-shelter { + --fa: "\e54f"; +} + +.fa-neuter { + --fa: "\f22c"; +} + +.fa-id-badge { + --fa: "\f2c1"; +} + +.fa-marker { + --fa: "\f5a1"; +} + +.fa-face-laugh-beam { + --fa: "\f59a"; +} + +.fa-laugh-beam { + --fa: "\f59a"; +} + +.fa-helicopter-symbol { + --fa: "\e502"; +} + +.fa-universal-access { + --fa: "\f29a"; +} + +.fa-circle-chevron-up { + --fa: "\f139"; +} + +.fa-chevron-circle-up { + --fa: "\f139"; +} + +.fa-lari-sign { + --fa: "\e1c8"; +} + +.fa-volcano { + --fa: "\f770"; +} + +.fa-person-walking-dashed-line-arrow-right { + --fa: "\e553"; +} + +.fa-sterling-sign { + --fa: "\f154"; +} + +.fa-gbp { + --fa: "\f154"; +} + +.fa-pound-sign { + --fa: "\f154"; +} + +.fa-viruses { + --fa: "\e076"; +} + +.fa-square-person-confined { + --fa: "\e577"; +} + +.fa-user-tie { + --fa: "\f508"; +} + +.fa-arrow-down-long { + --fa: "\f175"; +} + +.fa-long-arrow-down { + --fa: "\f175"; +} + +.fa-tent-arrow-down-to-line { + --fa: "\e57e"; +} + +.fa-certificate { + --fa: "\f0a3"; +} + +.fa-reply-all { + --fa: "\f122"; +} + +.fa-mail-reply-all { + --fa: "\f122"; +} + +.fa-suitcase { + --fa: "\f0f2"; +} + +.fa-person-skating { + --fa: "\f7c5"; +} + +.fa-skating { + --fa: "\f7c5"; +} + +.fa-filter-circle-dollar { + --fa: "\f662"; +} + +.fa-funnel-dollar { + --fa: "\f662"; +} + +.fa-camera-retro { + --fa: "\f083"; +} + +.fa-circle-arrow-down { + --fa: "\f0ab"; +} + +.fa-arrow-circle-down { + --fa: "\f0ab"; +} + +.fa-file-import { + --fa: "\f56f"; +} + +.fa-arrow-right-to-file { + --fa: "\f56f"; +} + +.fa-square-arrow-up-right { + --fa: "\f14c"; +} + +.fa-external-link-square { + --fa: "\f14c"; +} + +.fa-box-open { + --fa: "\f49e"; +} + +.fa-scroll { + --fa: "\f70e"; +} + +.fa-spa { + --fa: "\f5bb"; +} + +.fa-location-pin-lock { + --fa: "\e51f"; +} + +.fa-pause { + --fa: "\f04c"; +} + +.fa-hill-avalanche { + --fa: "\e507"; +} + +.fa-temperature-empty { + --fa: "\f2cb"; +} + +.fa-temperature-0 { + --fa: "\f2cb"; +} + +.fa-thermometer-0 { + --fa: "\f2cb"; +} + +.fa-thermometer-empty { + --fa: "\f2cb"; +} + +.fa-bomb { + --fa: "\f1e2"; +} + +.fa-registered { + --fa: "\f25d"; +} + +.fa-address-card { + --fa: "\f2bb"; +} + +.fa-contact-card { + --fa: "\f2bb"; +} + +.fa-vcard { + --fa: "\f2bb"; +} + +.fa-scale-unbalanced-flip { + --fa: "\f516"; +} + +.fa-balance-scale-right { + --fa: "\f516"; +} + +.fa-subscript { + --fa: "\f12c"; +} + +.fa-diamond-turn-right { + --fa: "\f5eb"; +} + +.fa-directions { + --fa: "\f5eb"; +} + +.fa-burst { + --fa: "\e4dc"; +} + +.fa-house-laptop { + --fa: "\e066"; +} + +.fa-laptop-house { + --fa: "\e066"; +} + +.fa-face-tired { + --fa: "\f5c8"; +} + +.fa-tired { + --fa: "\f5c8"; +} + +.fa-money-bills { + --fa: "\e1f3"; +} + +.fa-smog { + --fa: "\f75f"; +} + +.fa-crutch { + --fa: "\f7f7"; +} + +.fa-cloud-arrow-up { + --fa: "\f0ee"; +} + +.fa-cloud-upload { + --fa: "\f0ee"; +} + +.fa-cloud-upload-alt { + --fa: "\f0ee"; +} + +.fa-palette { + --fa: "\f53f"; +} + +.fa-arrows-turn-right { + --fa: "\e4c0"; +} + +.fa-vest { + --fa: "\e085"; +} + +.fa-ferry { + --fa: "\e4ea"; +} + +.fa-arrows-down-to-people { + --fa: "\e4b9"; +} + +.fa-seedling { + --fa: "\f4d8"; +} + +.fa-sprout { + --fa: "\f4d8"; +} + +.fa-left-right { + --fa: "\f337"; +} + +.fa-arrows-alt-h { + --fa: "\f337"; +} + +.fa-boxes-packing { + --fa: "\e4c7"; +} + +.fa-circle-arrow-left { + --fa: "\f0a8"; +} + +.fa-arrow-circle-left { + --fa: "\f0a8"; +} + +.fa-group-arrows-rotate { + --fa: "\e4f6"; +} + +.fa-bowl-food { + --fa: "\e4c6"; +} + +.fa-candy-cane { + --fa: "\f786"; +} + +.fa-arrow-down-wide-short { + --fa: "\f160"; +} + +.fa-sort-amount-asc { + --fa: "\f160"; +} + +.fa-sort-amount-down { + --fa: "\f160"; +} + +.fa-cloud-bolt { + --fa: "\f76c"; +} + +.fa-thunderstorm { + --fa: "\f76c"; +} + +.fa-text-slash { + --fa: "\f87d"; +} + +.fa-remove-format { + --fa: "\f87d"; +} + +.fa-face-smile-wink { + --fa: "\f4da"; +} + +.fa-smile-wink { + --fa: "\f4da"; +} + +.fa-file-word { + --fa: "\f1c2"; +} + +.fa-file-powerpoint { + --fa: "\f1c4"; +} + +.fa-arrows-left-right { + --fa: "\f07e"; +} + +.fa-arrows-h { + --fa: "\f07e"; +} + +.fa-house-lock { + --fa: "\e510"; +} + +.fa-cloud-arrow-down { + --fa: "\f0ed"; +} + +.fa-cloud-download { + --fa: "\f0ed"; +} + +.fa-cloud-download-alt { + --fa: "\f0ed"; +} + +.fa-children { + --fa: "\e4e1"; +} + +.fa-chalkboard { + --fa: "\f51b"; +} + +.fa-blackboard { + --fa: "\f51b"; +} + +.fa-user-large-slash { + --fa: "\f4fa"; +} + +.fa-user-alt-slash { + --fa: "\f4fa"; +} + +.fa-envelope-open { + --fa: "\f2b6"; +} + +.fa-handshake-simple-slash { + --fa: "\e05f"; +} + +.fa-handshake-alt-slash { + --fa: "\e05f"; +} + +.fa-mattress-pillow { + --fa: "\e525"; +} + +.fa-guarani-sign { + --fa: "\e19a"; +} + +.fa-arrows-rotate { + --fa: "\f021"; +} + +.fa-refresh { + --fa: "\f021"; +} + +.fa-sync { + --fa: "\f021"; +} + +.fa-fire-extinguisher { + --fa: "\f134"; +} + +.fa-cruzeiro-sign { + --fa: "\e152"; +} + +.fa-greater-than-equal { + --fa: "\f532"; +} + +.fa-shield-halved { + --fa: "\f3ed"; +} + +.fa-shield-alt { + --fa: "\f3ed"; +} + +.fa-book-atlas { + --fa: "\f558"; +} + +.fa-atlas { + --fa: "\f558"; +} + +.fa-virus { + --fa: "\e074"; +} + +.fa-envelope-circle-check { + --fa: "\e4e8"; +} + +.fa-layer-group { + --fa: "\f5fd"; +} + +.fa-arrows-to-dot { + --fa: "\e4be"; +} + +.fa-archway { + --fa: "\f557"; +} + +.fa-heart-circle-check { + --fa: "\e4fd"; +} + +.fa-house-chimney-crack { + --fa: "\f6f1"; +} + +.fa-house-damage { + --fa: "\f6f1"; +} + +.fa-file-zipper { + --fa: "\f1c6"; +} + +.fa-file-archive { + --fa: "\f1c6"; +} + +.fa-square { + --fa: "\f0c8"; +} + +.fa-martini-glass-empty { + --fa: "\f000"; +} + +.fa-glass-martini { + --fa: "\f000"; +} + +.fa-couch { + --fa: "\f4b8"; +} + +.fa-cedi-sign { + --fa: "\e0df"; +} + +.fa-italic { + --fa: "\f033"; +} + +.fa-table-cells-column-lock { + --fa: "\e678"; +} + +.fa-church { + --fa: "\f51d"; +} + +.fa-comments-dollar { + --fa: "\f653"; +} + +.fa-democrat { + --fa: "\f747"; +} + +.fa-z { + --fa: "\5a"; +} + +.fa-person-skiing { + --fa: "\f7c9"; +} + +.fa-skiing { + --fa: "\f7c9"; +} + +.fa-road-lock { + --fa: "\e567"; +} + +.fa-a { + --fa: "\41"; +} + +.fa-temperature-arrow-down { + --fa: "\e03f"; +} + +.fa-temperature-down { + --fa: "\e03f"; +} + +.fa-feather-pointed { + --fa: "\f56b"; +} + +.fa-feather-alt { + --fa: "\f56b"; +} + +.fa-p { + --fa: "\50"; +} + +.fa-snowflake { + --fa: "\f2dc"; +} + +.fa-newspaper { + --fa: "\f1ea"; +} + +.fa-rectangle-ad { + --fa: "\f641"; +} + +.fa-ad { + --fa: "\f641"; +} + +.fa-circle-arrow-right { + --fa: "\f0a9"; +} + +.fa-arrow-circle-right { + --fa: "\f0a9"; +} + +.fa-filter-circle-xmark { + --fa: "\e17b"; +} + +.fa-locust { + --fa: "\e520"; +} + +.fa-sort { + --fa: "\f0dc"; +} + +.fa-unsorted { + --fa: "\f0dc"; +} + +.fa-list-ol { + --fa: "\f0cb"; +} + +.fa-list-1-2 { + --fa: "\f0cb"; +} + +.fa-list-numeric { + --fa: "\f0cb"; +} + +.fa-person-dress-burst { + --fa: "\e544"; +} + +.fa-money-check-dollar { + --fa: "\f53d"; +} + +.fa-money-check-alt { + --fa: "\f53d"; +} + +.fa-vector-square { + --fa: "\f5cb"; +} + +.fa-bread-slice { + --fa: "\f7ec"; +} + +.fa-language { + --fa: "\f1ab"; +} + +.fa-face-kiss-wink-heart { + --fa: "\f598"; +} + +.fa-kiss-wink-heart { + --fa: "\f598"; +} + +.fa-filter { + --fa: "\f0b0"; +} + +.fa-question { + --fa: "\3f"; +} + +.fa-file-signature { + --fa: "\f573"; +} + +.fa-up-down-left-right { + --fa: "\f0b2"; +} + +.fa-arrows-alt { + --fa: "\f0b2"; +} + +.fa-house-chimney-user { + --fa: "\e065"; +} + +.fa-hand-holding-heart { + --fa: "\f4be"; +} + +.fa-puzzle-piece { + --fa: "\f12e"; +} + +.fa-money-check { + --fa: "\f53c"; +} + +.fa-star-half-stroke { + --fa: "\f5c0"; +} + +.fa-star-half-alt { + --fa: "\f5c0"; +} + +.fa-code { + --fa: "\f121"; +} + +.fa-whiskey-glass { + --fa: "\f7a0"; +} + +.fa-glass-whiskey { + --fa: "\f7a0"; +} + +.fa-building-circle-exclamation { + --fa: "\e4d3"; +} + +.fa-magnifying-glass-chart { + --fa: "\e522"; +} + +.fa-arrow-up-right-from-square { + --fa: "\f08e"; +} + +.fa-external-link { + --fa: "\f08e"; +} + +.fa-cubes-stacked { + --fa: "\e4e6"; +} + +.fa-won-sign { + --fa: "\f159"; +} + +.fa-krw { + --fa: "\f159"; +} + +.fa-won { + --fa: "\f159"; +} + +.fa-virus-covid { + --fa: "\e4a8"; +} + +.fa-austral-sign { + --fa: "\e0a9"; +} + +.fa-f { + --fa: "\46"; +} + +.fa-leaf { + --fa: "\f06c"; +} + +.fa-road { + --fa: "\f018"; +} + +.fa-taxi { + --fa: "\f1ba"; +} + +.fa-cab { + --fa: "\f1ba"; +} + +.fa-person-circle-plus { + --fa: "\e541"; +} + +.fa-chart-pie { + --fa: "\f200"; +} + +.fa-pie-chart { + --fa: "\f200"; +} + +.fa-bolt-lightning { + --fa: "\e0b7"; +} + +.fa-sack-xmark { + --fa: "\e56a"; +} + +.fa-file-excel { + --fa: "\f1c3"; +} + +.fa-file-contract { + --fa: "\f56c"; +} + +.fa-fish-fins { + --fa: "\e4f2"; +} + +.fa-building-flag { + --fa: "\e4d5"; +} + +.fa-face-grin-beam { + --fa: "\f582"; +} + +.fa-grin-beam { + --fa: "\f582"; +} + +.fa-object-ungroup { + --fa: "\f248"; +} + +.fa-poop { + --fa: "\f619"; +} + +.fa-location-pin { + --fa: "\f041"; +} + +.fa-map-marker { + --fa: "\f041"; +} + +.fa-kaaba { + --fa: "\f66b"; +} + +.fa-toilet-paper { + --fa: "\f71e"; +} + +.fa-helmet-safety { + --fa: "\f807"; +} + +.fa-hard-hat { + --fa: "\f807"; +} + +.fa-hat-hard { + --fa: "\f807"; +} + +.fa-eject { + --fa: "\f052"; +} + +.fa-circle-right { + --fa: "\f35a"; +} + +.fa-arrow-alt-circle-right { + --fa: "\f35a"; +} + +.fa-plane-circle-check { + --fa: "\e555"; +} + +.fa-face-rolling-eyes { + --fa: "\f5a5"; +} + +.fa-meh-rolling-eyes { + --fa: "\f5a5"; +} + +.fa-object-group { + --fa: "\f247"; +} + +.fa-chart-line { + --fa: "\f201"; +} + +.fa-line-chart { + --fa: "\f201"; +} + +.fa-mask-ventilator { + --fa: "\e524"; +} + +.fa-arrow-right { + --fa: "\f061"; +} + +.fa-signs-post { + --fa: "\f277"; +} + +.fa-map-signs { + --fa: "\f277"; +} + +.fa-cash-register { + --fa: "\f788"; +} + +.fa-person-circle-question { + --fa: "\e542"; +} + +.fa-h { + --fa: "\48"; +} + +.fa-tarp { + --fa: "\e57b"; +} + +.fa-screwdriver-wrench { + --fa: "\f7d9"; +} + +.fa-tools { + --fa: "\f7d9"; +} + +.fa-arrows-to-eye { + --fa: "\e4bf"; +} + +.fa-plug-circle-bolt { + --fa: "\e55b"; +} + +.fa-heart { + --fa: "\f004"; +} + +.fa-mars-and-venus { + --fa: "\f224"; +} + +.fa-house-user { + --fa: "\e1b0"; +} + +.fa-home-user { + --fa: "\e1b0"; +} + +.fa-dumpster-fire { + --fa: "\f794"; +} + +.fa-house-crack { + --fa: "\e3b1"; +} + +.fa-martini-glass-citrus { + --fa: "\f561"; +} + +.fa-cocktail { + --fa: "\f561"; +} + +.fa-face-surprise { + --fa: "\f5c2"; +} + +.fa-surprise { + --fa: "\f5c2"; +} + +.fa-bottle-water { + --fa: "\e4c5"; +} + +.fa-circle-pause { + --fa: "\f28b"; +} + +.fa-pause-circle { + --fa: "\f28b"; +} + +.fa-toilet-paper-slash { + --fa: "\e072"; +} + +.fa-apple-whole { + --fa: "\f5d1"; +} + +.fa-apple-alt { + --fa: "\f5d1"; +} + +.fa-kitchen-set { + --fa: "\e51a"; +} + +.fa-r { + --fa: "\52"; +} + +.fa-temperature-quarter { + --fa: "\f2ca"; +} + +.fa-temperature-1 { + --fa: "\f2ca"; +} + +.fa-thermometer-1 { + --fa: "\f2ca"; +} + +.fa-thermometer-quarter { + --fa: "\f2ca"; +} + +.fa-cube { + --fa: "\f1b2"; +} + +.fa-bitcoin-sign { + --fa: "\e0b4"; +} + +.fa-shield-dog { + --fa: "\e573"; +} + +.fa-solar-panel { + --fa: "\f5ba"; +} + +.fa-lock-open { + --fa: "\f3c1"; +} + +.fa-elevator { + --fa: "\e16d"; +} + +.fa-money-bill-transfer { + --fa: "\e528"; +} + +.fa-money-bill-trend-up { + --fa: "\e529"; +} + +.fa-house-flood-water-circle-arrow-right { + --fa: "\e50f"; +} + +.fa-square-poll-horizontal { + --fa: "\f682"; +} + +.fa-poll-h { + --fa: "\f682"; +} + +.fa-circle { + --fa: "\f111"; +} + +.fa-backward-fast { + --fa: "\f049"; +} + +.fa-fast-backward { + --fa: "\f049"; +} + +.fa-recycle { + --fa: "\f1b8"; +} + +.fa-user-astronaut { + --fa: "\f4fb"; +} + +.fa-plane-slash { + --fa: "\e069"; +} + +.fa-trademark { + --fa: "\f25c"; +} + +.fa-basketball { + --fa: "\f434"; +} + +.fa-basketball-ball { + --fa: "\f434"; +} + +.fa-satellite-dish { + --fa: "\f7c0"; +} + +.fa-circle-up { + --fa: "\f35b"; +} + +.fa-arrow-alt-circle-up { + --fa: "\f35b"; +} + +.fa-mobile-screen-button { + --fa: "\f3cd"; +} + +.fa-mobile-alt { + --fa: "\f3cd"; +} + +.fa-volume-high { + --fa: "\f028"; +} + +.fa-volume-up { + --fa: "\f028"; +} + +.fa-users-rays { + --fa: "\e593"; +} + +.fa-wallet { + --fa: "\f555"; +} + +.fa-clipboard-check { + --fa: "\f46c"; +} + +.fa-file-audio { + --fa: "\f1c7"; +} + +.fa-burger { + --fa: "\f805"; +} + +.fa-hamburger { + --fa: "\f805"; +} + +.fa-wrench { + --fa: "\f0ad"; +} + +.fa-bugs { + --fa: "\e4d0"; +} + +.fa-rupee-sign { + --fa: "\f156"; +} + +.fa-rupee { + --fa: "\f156"; +} + +.fa-file-image { + --fa: "\f1c5"; +} + +.fa-circle-question { + --fa: "\f059"; +} + +.fa-question-circle { + --fa: "\f059"; +} + +.fa-plane-departure { + --fa: "\f5b0"; +} + +.fa-handshake-slash { + --fa: "\e060"; +} + +.fa-book-bookmark { + --fa: "\e0bb"; +} + +.fa-code-branch { + --fa: "\f126"; +} + +.fa-hat-cowboy { + --fa: "\f8c0"; +} + +.fa-bridge { + --fa: "\e4c8"; +} + +.fa-phone-flip { + --fa: "\f879"; +} + +.fa-phone-alt { + --fa: "\f879"; +} + +.fa-truck-front { + --fa: "\e2b7"; +} + +.fa-cat { + --fa: "\f6be"; +} + +.fa-anchor-circle-exclamation { + --fa: "\e4ab"; +} + +.fa-truck-field { + --fa: "\e58d"; +} + +.fa-route { + --fa: "\f4d7"; +} + +.fa-clipboard-question { + --fa: "\e4e3"; +} + +.fa-panorama { + --fa: "\e209"; +} + +.fa-comment-medical { + --fa: "\f7f5"; +} + +.fa-teeth-open { + --fa: "\f62f"; +} + +.fa-file-circle-minus { + --fa: "\e4ed"; +} + +.fa-tags { + --fa: "\f02c"; +} + +.fa-wine-glass { + --fa: "\f4e3"; +} + +.fa-forward-fast { + --fa: "\f050"; +} + +.fa-fast-forward { + --fa: "\f050"; +} + +.fa-face-meh-blank { + --fa: "\f5a4"; +} + +.fa-meh-blank { + --fa: "\f5a4"; +} + +.fa-square-parking { + --fa: "\f540"; +} + +.fa-parking { + --fa: "\f540"; +} + +.fa-house-signal { + --fa: "\e012"; +} + +.fa-bars-progress { + --fa: "\f828"; +} + +.fa-tasks-alt { + --fa: "\f828"; +} + +.fa-faucet-drip { + --fa: "\e006"; +} + +.fa-cart-flatbed { + --fa: "\f474"; +} + +.fa-dolly-flatbed { + --fa: "\f474"; +} + +.fa-ban-smoking { + --fa: "\f54d"; +} + +.fa-smoking-ban { + --fa: "\f54d"; +} + +.fa-terminal { + --fa: "\f120"; +} + +.fa-mobile-button { + --fa: "\f10b"; +} + +.fa-house-medical-flag { + --fa: "\e514"; +} + +.fa-basket-shopping { + --fa: "\f291"; +} + +.fa-shopping-basket { + --fa: "\f291"; +} + +.fa-tape { + --fa: "\f4db"; +} + +.fa-bus-simple { + --fa: "\f55e"; +} + +.fa-bus-alt { + --fa: "\f55e"; +} + +.fa-eye { + --fa: "\f06e"; +} + +.fa-face-sad-cry { + --fa: "\f5b3"; +} + +.fa-sad-cry { + --fa: "\f5b3"; +} + +.fa-audio-description { + --fa: "\f29e"; +} + +.fa-person-military-to-person { + --fa: "\e54c"; +} + +.fa-file-shield { + --fa: "\e4f0"; +} + +.fa-user-slash { + --fa: "\f506"; +} + +.fa-pen { + --fa: "\f304"; +} + +.fa-tower-observation { + --fa: "\e586"; +} + +.fa-file-code { + --fa: "\f1c9"; +} + +.fa-signal { + --fa: "\f012"; +} + +.fa-signal-5 { + --fa: "\f012"; +} + +.fa-signal-perfect { + --fa: "\f012"; +} + +.fa-bus { + --fa: "\f207"; +} + +.fa-heart-circle-xmark { + --fa: "\e501"; +} + +.fa-house-chimney { + --fa: "\e3af"; +} + +.fa-home-lg { + --fa: "\e3af"; +} + +.fa-window-maximize { + --fa: "\f2d0"; +} + +.fa-face-frown { + --fa: "\f119"; +} + +.fa-frown { + --fa: "\f119"; +} + +.fa-prescription { + --fa: "\f5b1"; +} + +.fa-shop { + --fa: "\f54f"; +} + +.fa-store-alt { + --fa: "\f54f"; +} + +.fa-floppy-disk { + --fa: "\f0c7"; +} + +.fa-save { + --fa: "\f0c7"; +} + +.fa-vihara { + --fa: "\f6a7"; +} + +.fa-scale-unbalanced { + --fa: "\f515"; +} + +.fa-balance-scale-left { + --fa: "\f515"; +} + +.fa-sort-up { + --fa: "\f0de"; +} + +.fa-sort-asc { + --fa: "\f0de"; +} + +.fa-comment-dots { + --fa: "\f4ad"; +} + +.fa-commenting { + --fa: "\f4ad"; +} + +.fa-plant-wilt { + --fa: "\e5aa"; +} + +.fa-diamond { + --fa: "\f219"; +} + +.fa-face-grin-squint { + --fa: "\f585"; +} + +.fa-grin-squint { + --fa: "\f585"; +} + +.fa-hand-holding-dollar { + --fa: "\f4c0"; +} + +.fa-hand-holding-usd { + --fa: "\f4c0"; +} + +.fa-chart-diagram { + --fa: "\e695"; +} + +.fa-bacterium { + --fa: "\e05a"; +} + +.fa-hand-pointer { + --fa: "\f25a"; +} + +.fa-drum-steelpan { + --fa: "\f56a"; +} + +.fa-hand-scissors { + --fa: "\f257"; +} + +.fa-hands-praying { + --fa: "\f684"; +} + +.fa-praying-hands { + --fa: "\f684"; +} + +.fa-arrow-rotate-right { + --fa: "\f01e"; +} + +.fa-arrow-right-rotate { + --fa: "\f01e"; +} + +.fa-arrow-rotate-forward { + --fa: "\f01e"; +} + +.fa-redo { + --fa: "\f01e"; +} + +.fa-biohazard { + --fa: "\f780"; +} + +.fa-location-crosshairs { + --fa: "\f601"; +} + +.fa-location { + --fa: "\f601"; +} + +.fa-mars-double { + --fa: "\f227"; +} + +.fa-child-dress { + --fa: "\e59c"; +} + +.fa-users-between-lines { + --fa: "\e591"; +} + +.fa-lungs-virus { + --fa: "\e067"; +} + +.fa-face-grin-tears { + --fa: "\f588"; +} + +.fa-grin-tears { + --fa: "\f588"; +} + +.fa-phone { + --fa: "\f095"; +} + +.fa-calendar-xmark { + --fa: "\f273"; +} + +.fa-calendar-times { + --fa: "\f273"; +} + +.fa-child-reaching { + --fa: "\e59d"; +} + +.fa-head-side-virus { + --fa: "\e064"; +} + +.fa-user-gear { + --fa: "\f4fe"; +} + +.fa-user-cog { + --fa: "\f4fe"; +} + +.fa-arrow-up-1-9 { + --fa: "\f163"; +} + +.fa-sort-numeric-up { + --fa: "\f163"; +} + +.fa-door-closed { + --fa: "\f52a"; +} + +.fa-shield-virus { + --fa: "\e06c"; +} + +.fa-dice-six { + --fa: "\f526"; +} + +.fa-mosquito-net { + --fa: "\e52c"; +} + +.fa-file-fragment { + --fa: "\e697"; +} + +.fa-bridge-water { + --fa: "\e4ce"; +} + +.fa-person-booth { + --fa: "\f756"; +} + +.fa-text-width { + --fa: "\f035"; +} + +.fa-hat-wizard { + --fa: "\f6e8"; +} + +.fa-pen-fancy { + --fa: "\f5ac"; +} + +.fa-person-digging { + --fa: "\f85e"; +} + +.fa-digging { + --fa: "\f85e"; +} + +.fa-trash { + --fa: "\f1f8"; +} + +.fa-gauge-simple { + --fa: "\f629"; +} + +.fa-gauge-simple-med { + --fa: "\f629"; +} + +.fa-tachometer-average { + --fa: "\f629"; +} + +.fa-book-medical { + --fa: "\f7e6"; +} + +.fa-poo { + --fa: "\f2fe"; +} + +.fa-quote-right { + --fa: "\f10e"; +} + +.fa-quote-right-alt { + --fa: "\f10e"; +} + +.fa-shirt { + --fa: "\f553"; +} + +.fa-t-shirt { + --fa: "\f553"; +} + +.fa-tshirt { + --fa: "\f553"; +} + +.fa-cubes { + --fa: "\f1b3"; +} + +.fa-divide { + --fa: "\f529"; +} + +.fa-tenge-sign { + --fa: "\f7d7"; +} + +.fa-tenge { + --fa: "\f7d7"; +} + +.fa-headphones { + --fa: "\f025"; +} + +.fa-hands-holding { + --fa: "\f4c2"; +} + +.fa-hands-clapping { + --fa: "\e1a8"; +} + +.fa-republican { + --fa: "\f75e"; +} + +.fa-arrow-left { + --fa: "\f060"; +} + +.fa-person-circle-xmark { + --fa: "\e543"; +} + +.fa-ruler { + --fa: "\f545"; +} + +.fa-align-left { + --fa: "\f036"; +} + +.fa-dice-d6 { + --fa: "\f6d1"; +} + +.fa-restroom { + --fa: "\f7bd"; +} + +.fa-j { + --fa: "\4a"; +} + +.fa-users-viewfinder { + --fa: "\e595"; +} + +.fa-file-video { + --fa: "\f1c8"; +} + +.fa-up-right-from-square { + --fa: "\f35d"; +} + +.fa-external-link-alt { + --fa: "\f35d"; +} + +.fa-table-cells { + --fa: "\f00a"; +} + +.fa-th { + --fa: "\f00a"; +} + +.fa-file-pdf { + --fa: "\f1c1"; +} + +.fa-book-bible { + --fa: "\f647"; +} + +.fa-bible { + --fa: "\f647"; +} + +.fa-o { + --fa: "\4f"; +} + +.fa-suitcase-medical { + --fa: "\f0fa"; +} + +.fa-medkit { + --fa: "\f0fa"; +} + +.fa-user-secret { + --fa: "\f21b"; +} + +.fa-otter { + --fa: "\f700"; +} + +.fa-person-dress { + --fa: "\f182"; +} + +.fa-female { + --fa: "\f182"; +} + +.fa-comment-dollar { + --fa: "\f651"; +} + +.fa-business-time { + --fa: "\f64a"; +} + +.fa-briefcase-clock { + --fa: "\f64a"; +} + +.fa-table-cells-large { + --fa: "\f009"; +} + +.fa-th-large { + --fa: "\f009"; +} + +.fa-book-tanakh { + --fa: "\f827"; +} + +.fa-tanakh { + --fa: "\f827"; +} + +.fa-phone-volume { + --fa: "\f2a0"; +} + +.fa-volume-control-phone { + --fa: "\f2a0"; +} + +.fa-hat-cowboy-side { + --fa: "\f8c1"; +} + +.fa-clipboard-user { + --fa: "\f7f3"; +} + +.fa-child { + --fa: "\f1ae"; +} + +.fa-lira-sign { + --fa: "\f195"; +} + +.fa-satellite { + --fa: "\f7bf"; +} + +.fa-plane-lock { + --fa: "\e558"; +} + +.fa-tag { + --fa: "\f02b"; +} + +.fa-comment { + --fa: "\f075"; +} + +.fa-cake-candles { + --fa: "\f1fd"; +} + +.fa-birthday-cake { + --fa: "\f1fd"; +} + +.fa-cake { + --fa: "\f1fd"; +} + +.fa-envelope { + --fa: "\f0e0"; +} + +.fa-angles-up { + --fa: "\f102"; +} + +.fa-angle-double-up { + --fa: "\f102"; +} + +.fa-paperclip { + --fa: "\f0c6"; +} + +.fa-arrow-right-to-city { + --fa: "\e4b3"; +} + +.fa-ribbon { + --fa: "\f4d6"; +} + +.fa-lungs { + --fa: "\f604"; +} + +.fa-arrow-up-9-1 { + --fa: "\f887"; +} + +.fa-sort-numeric-up-alt { + --fa: "\f887"; +} + +.fa-litecoin-sign { + --fa: "\e1d3"; +} + +.fa-border-none { + --fa: "\f850"; +} + +.fa-circle-nodes { + --fa: "\e4e2"; +} + +.fa-parachute-box { + --fa: "\f4cd"; +} + +.fa-indent { + --fa: "\f03c"; +} + +.fa-truck-field-un { + --fa: "\e58e"; +} + +.fa-hourglass { + --fa: "\f254"; +} + +.fa-hourglass-empty { + --fa: "\f254"; +} + +.fa-mountain { + --fa: "\f6fc"; +} + +.fa-user-doctor { + --fa: "\f0f0"; +} + +.fa-user-md { + --fa: "\f0f0"; +} + +.fa-circle-info { + --fa: "\f05a"; +} + +.fa-info-circle { + --fa: "\f05a"; +} + +.fa-cloud-meatball { + --fa: "\f73b"; +} + +.fa-camera { + --fa: "\f030"; +} + +.fa-camera-alt { + --fa: "\f030"; +} + +.fa-square-virus { + --fa: "\e578"; +} + +.fa-meteor { + --fa: "\f753"; +} + +.fa-car-on { + --fa: "\e4dd"; +} + +.fa-sleigh { + --fa: "\f7cc"; +} + +.fa-arrow-down-1-9 { + --fa: "\f162"; +} + +.fa-sort-numeric-asc { + --fa: "\f162"; +} + +.fa-sort-numeric-down { + --fa: "\f162"; +} + +.fa-hand-holding-droplet { + --fa: "\f4c1"; +} + +.fa-hand-holding-water { + --fa: "\f4c1"; +} + +.fa-water { + --fa: "\f773"; +} + +.fa-calendar-check { + --fa: "\f274"; +} + +.fa-braille { + --fa: "\f2a1"; +} + +.fa-prescription-bottle-medical { + --fa: "\f486"; +} + +.fa-prescription-bottle-alt { + --fa: "\f486"; +} + +.fa-landmark { + --fa: "\f66f"; +} + +.fa-truck { + --fa: "\f0d1"; +} + +.fa-crosshairs { + --fa: "\f05b"; +} + +.fa-person-cane { + --fa: "\e53c"; +} + +.fa-tent { + --fa: "\e57d"; +} + +.fa-vest-patches { + --fa: "\e086"; +} + +.fa-check-double { + --fa: "\f560"; +} + +.fa-arrow-down-a-z { + --fa: "\f15d"; +} + +.fa-sort-alpha-asc { + --fa: "\f15d"; +} + +.fa-sort-alpha-down { + --fa: "\f15d"; +} + +.fa-money-bill-wheat { + --fa: "\e52a"; +} + +.fa-cookie { + --fa: "\f563"; +} + +.fa-arrow-rotate-left { + --fa: "\f0e2"; +} + +.fa-arrow-left-rotate { + --fa: "\f0e2"; +} + +.fa-arrow-rotate-back { + --fa: "\f0e2"; +} + +.fa-arrow-rotate-backward { + --fa: "\f0e2"; +} + +.fa-undo { + --fa: "\f0e2"; +} + +.fa-hard-drive { + --fa: "\f0a0"; +} + +.fa-hdd { + --fa: "\f0a0"; +} + +.fa-face-grin-squint-tears { + --fa: "\f586"; +} + +.fa-grin-squint-tears { + --fa: "\f586"; +} + +.fa-dumbbell { + --fa: "\f44b"; +} + +.fa-rectangle-list { + --fa: "\f022"; +} + +.fa-list-alt { + --fa: "\f022"; +} + +.fa-tarp-droplet { + --fa: "\e57c"; +} + +.fa-house-medical-circle-check { + --fa: "\e511"; +} + +.fa-person-skiing-nordic { + --fa: "\f7ca"; +} + +.fa-skiing-nordic { + --fa: "\f7ca"; +} + +.fa-calendar-plus { + --fa: "\f271"; +} + +.fa-plane-arrival { + --fa: "\f5af"; +} + +.fa-circle-left { + --fa: "\f359"; +} + +.fa-arrow-alt-circle-left { + --fa: "\f359"; +} + +.fa-train-subway { + --fa: "\f239"; +} + +.fa-subway { + --fa: "\f239"; +} + +.fa-chart-gantt { + --fa: "\e0e4"; +} + +.fa-indian-rupee-sign { + --fa: "\e1bc"; +} + +.fa-indian-rupee { + --fa: "\e1bc"; +} + +.fa-inr { + --fa: "\e1bc"; +} + +.fa-crop-simple { + --fa: "\f565"; +} + +.fa-crop-alt { + --fa: "\f565"; +} + +.fa-money-bill-1 { + --fa: "\f3d1"; +} + +.fa-money-bill-alt { + --fa: "\f3d1"; +} + +.fa-left-long { + --fa: "\f30a"; +} + +.fa-long-arrow-alt-left { + --fa: "\f30a"; +} + +.fa-dna { + --fa: "\f471"; +} + +.fa-virus-slash { + --fa: "\e075"; +} + +.fa-minus { + --fa: "\f068"; +} + +.fa-subtract { + --fa: "\f068"; +} + +.fa-chess { + --fa: "\f439"; +} + +.fa-arrow-left-long { + --fa: "\f177"; +} + +.fa-long-arrow-left { + --fa: "\f177"; +} + +.fa-plug-circle-check { + --fa: "\e55c"; +} + +.fa-street-view { + --fa: "\f21d"; +} + +.fa-franc-sign { + --fa: "\e18f"; +} + +.fa-volume-off { + --fa: "\f026"; +} + +.fa-hands-asl-interpreting { + --fa: "\f2a3"; +} + +.fa-american-sign-language-interpreting { + --fa: "\f2a3"; +} + +.fa-asl-interpreting { + --fa: "\f2a3"; +} + +.fa-hands-american-sign-language-interpreting { + --fa: "\f2a3"; +} + +.fa-gear { + --fa: "\f013"; +} + +.fa-cog { + --fa: "\f013"; +} + +.fa-droplet-slash { + --fa: "\f5c7"; +} + +.fa-tint-slash { + --fa: "\f5c7"; +} + +.fa-mosque { + --fa: "\f678"; +} + +.fa-mosquito { + --fa: "\e52b"; +} + +.fa-star-of-david { + --fa: "\f69a"; +} + +.fa-person-military-rifle { + --fa: "\e54b"; +} + +.fa-cart-shopping { + --fa: "\f07a"; +} + +.fa-shopping-cart { + --fa: "\f07a"; +} + +.fa-vials { + --fa: "\f493"; +} + +.fa-plug-circle-plus { + --fa: "\e55f"; +} + +.fa-place-of-worship { + --fa: "\f67f"; +} + +.fa-grip-vertical { + --fa: "\f58e"; +} + +.fa-hexagon-nodes { + --fa: "\e699"; +} + +.fa-arrow-turn-up { + --fa: "\f148"; +} + +.fa-level-up { + --fa: "\f148"; +} + +.fa-u { + --fa: "\55"; +} + +.fa-square-root-variable { + --fa: "\f698"; +} + +.fa-square-root-alt { + --fa: "\f698"; +} + +.fa-clock { + --fa: "\f017"; +} + +.fa-clock-four { + --fa: "\f017"; +} + +.fa-backward-step { + --fa: "\f048"; +} + +.fa-step-backward { + --fa: "\f048"; +} + +.fa-pallet { + --fa: "\f482"; +} + +.fa-faucet { + --fa: "\e005"; +} + +.fa-baseball-bat-ball { + --fa: "\f432"; +} + +.fa-s { + --fa: "\53"; +} + +.fa-timeline { + --fa: "\e29c"; +} + +.fa-keyboard { + --fa: "\f11c"; +} + +.fa-caret-down { + --fa: "\f0d7"; +} + +.fa-house-chimney-medical { + --fa: "\f7f2"; +} + +.fa-clinic-medical { + --fa: "\f7f2"; +} + +.fa-temperature-three-quarters { + --fa: "\f2c8"; +} + +.fa-temperature-3 { + --fa: "\f2c8"; +} + +.fa-thermometer-3 { + --fa: "\f2c8"; +} + +.fa-thermometer-three-quarters { + --fa: "\f2c8"; +} + +.fa-mobile-screen { + --fa: "\f3cf"; +} + +.fa-mobile-android-alt { + --fa: "\f3cf"; +} + +.fa-plane-up { + --fa: "\e22d"; +} + +.fa-piggy-bank { + --fa: "\f4d3"; +} + +.fa-battery-half { + --fa: "\f242"; +} + +.fa-battery-3 { + --fa: "\f242"; +} + +.fa-mountain-city { + --fa: "\e52e"; +} + +.fa-coins { + --fa: "\f51e"; +} + +.fa-khanda { + --fa: "\f66d"; +} + +.fa-sliders { + --fa: "\f1de"; +} + +.fa-sliders-h { + --fa: "\f1de"; +} + +.fa-folder-tree { + --fa: "\f802"; +} + +.fa-network-wired { + --fa: "\f6ff"; +} + +.fa-map-pin { + --fa: "\f276"; +} + +.fa-hamsa { + --fa: "\f665"; +} + +.fa-cent-sign { + --fa: "\e3f5"; +} + +.fa-flask { + --fa: "\f0c3"; +} + +.fa-person-pregnant { + --fa: "\e31e"; +} + +.fa-wand-sparkles { + --fa: "\f72b"; +} + +.fa-ellipsis-vertical { + --fa: "\f142"; +} + +.fa-ellipsis-v { + --fa: "\f142"; +} + +.fa-ticket { + --fa: "\f145"; +} + +.fa-power-off { + --fa: "\f011"; +} + +.fa-right-long { + --fa: "\f30b"; +} + +.fa-long-arrow-alt-right { + --fa: "\f30b"; +} + +.fa-flag-usa { + --fa: "\f74d"; +} + +.fa-laptop-file { + --fa: "\e51d"; +} + +.fa-tty { + --fa: "\f1e4"; +} + +.fa-teletype { + --fa: "\f1e4"; +} + +.fa-diagram-next { + --fa: "\e476"; +} + +.fa-person-rifle { + --fa: "\e54e"; +} + +.fa-house-medical-circle-exclamation { + --fa: "\e512"; +} + +.fa-closed-captioning { + --fa: "\f20a"; +} + +.fa-person-hiking { + --fa: "\f6ec"; +} + +.fa-hiking { + --fa: "\f6ec"; +} + +.fa-venus-double { + --fa: "\f226"; +} + +.fa-images { + --fa: "\f302"; +} + +.fa-calculator { + --fa: "\f1ec"; +} + +.fa-people-pulling { + --fa: "\e535"; +} + +.fa-n { + --fa: "\4e"; +} + +.fa-cable-car { + --fa: "\f7da"; +} + +.fa-tram { + --fa: "\f7da"; +} + +.fa-cloud-rain { + --fa: "\f73d"; +} + +.fa-building-circle-xmark { + --fa: "\e4d4"; +} + +.fa-ship { + --fa: "\f21a"; +} + +.fa-arrows-down-to-line { + --fa: "\e4b8"; +} + +.fa-download { + --fa: "\f019"; +} + +.fa-face-grin { + --fa: "\f580"; +} + +.fa-grin { + --fa: "\f580"; +} + +.fa-delete-left { + --fa: "\f55a"; +} + +.fa-backspace { + --fa: "\f55a"; +} + +.fa-eye-dropper { + --fa: "\f1fb"; +} + +.fa-eye-dropper-empty { + --fa: "\f1fb"; +} + +.fa-eyedropper { + --fa: "\f1fb"; +} + +.fa-file-circle-check { + --fa: "\e5a0"; +} + +.fa-forward { + --fa: "\f04e"; +} + +.fa-mobile { + --fa: "\f3ce"; +} + +.fa-mobile-android { + --fa: "\f3ce"; +} + +.fa-mobile-phone { + --fa: "\f3ce"; +} + +.fa-face-meh { + --fa: "\f11a"; +} + +.fa-meh { + --fa: "\f11a"; +} + +.fa-align-center { + --fa: "\f037"; +} + +.fa-book-skull { + --fa: "\f6b7"; +} + +.fa-book-dead { + --fa: "\f6b7"; +} + +.fa-id-card { + --fa: "\f2c2"; +} + +.fa-drivers-license { + --fa: "\f2c2"; +} + +.fa-outdent { + --fa: "\f03b"; +} + +.fa-dedent { + --fa: "\f03b"; +} + +.fa-heart-circle-exclamation { + --fa: "\e4fe"; +} + +.fa-house { + --fa: "\f015"; +} + +.fa-home { + --fa: "\f015"; +} + +.fa-home-alt { + --fa: "\f015"; +} + +.fa-home-lg-alt { + --fa: "\f015"; +} + +.fa-calendar-week { + --fa: "\f784"; +} + +.fa-laptop-medical { + --fa: "\f812"; +} + +.fa-b { + --fa: "\42"; +} + +.fa-file-medical { + --fa: "\f477"; +} + +.fa-dice-one { + --fa: "\f525"; +} + +.fa-kiwi-bird { + --fa: "\f535"; +} + +.fa-arrow-right-arrow-left { + --fa: "\f0ec"; +} + +.fa-exchange { + --fa: "\f0ec"; +} + +.fa-rotate-right { + --fa: "\f2f9"; +} + +.fa-redo-alt { + --fa: "\f2f9"; +} + +.fa-rotate-forward { + --fa: "\f2f9"; +} + +.fa-utensils { + --fa: "\f2e7"; +} + +.fa-cutlery { + --fa: "\f2e7"; +} + +.fa-arrow-up-wide-short { + --fa: "\f161"; +} + +.fa-sort-amount-up { + --fa: "\f161"; +} + +.fa-mill-sign { + --fa: "\e1ed"; +} + +.fa-bowl-rice { + --fa: "\e2eb"; +} + +.fa-skull { + --fa: "\f54c"; +} + +.fa-tower-broadcast { + --fa: "\f519"; +} + +.fa-broadcast-tower { + --fa: "\f519"; +} + +.fa-truck-pickup { + --fa: "\f63c"; +} + +.fa-up-long { + --fa: "\f30c"; +} + +.fa-long-arrow-alt-up { + --fa: "\f30c"; +} + +.fa-stop { + --fa: "\f04d"; +} + +.fa-code-merge { + --fa: "\f387"; +} + +.fa-upload { + --fa: "\f093"; +} + +.fa-hurricane { + --fa: "\f751"; +} + +.fa-mound { + --fa: "\e52d"; +} + +.fa-toilet-portable { + --fa: "\e583"; +} + +.fa-compact-disc { + --fa: "\f51f"; +} + +.fa-file-arrow-down { + --fa: "\f56d"; +} + +.fa-file-download { + --fa: "\f56d"; +} + +.fa-caravan { + --fa: "\f8ff"; +} + +.fa-shield-cat { + --fa: "\e572"; +} + +.fa-bolt { + --fa: "\f0e7"; +} + +.fa-zap { + --fa: "\f0e7"; +} + +.fa-glass-water { + --fa: "\e4f4"; +} + +.fa-oil-well { + --fa: "\e532"; +} + +.fa-vault { + --fa: "\e2c5"; +} + +.fa-mars { + --fa: "\f222"; +} + +.fa-toilet { + --fa: "\f7d8"; +} + +.fa-plane-circle-xmark { + --fa: "\e557"; +} + +.fa-yen-sign { + --fa: "\f157"; +} + +.fa-cny { + --fa: "\f157"; +} + +.fa-jpy { + --fa: "\f157"; +} + +.fa-rmb { + --fa: "\f157"; +} + +.fa-yen { + --fa: "\f157"; +} + +.fa-ruble-sign { + --fa: "\f158"; +} + +.fa-rouble { + --fa: "\f158"; +} + +.fa-rub { + --fa: "\f158"; +} + +.fa-ruble { + --fa: "\f158"; +} + +.fa-sun { + --fa: "\f185"; +} + +.fa-guitar { + --fa: "\f7a6"; +} + +.fa-face-laugh-wink { + --fa: "\f59c"; +} + +.fa-laugh-wink { + --fa: "\f59c"; +} + +.fa-horse-head { + --fa: "\f7ab"; +} + +.fa-bore-hole { + --fa: "\e4c3"; +} + +.fa-industry { + --fa: "\f275"; +} + +.fa-circle-down { + --fa: "\f358"; +} + +.fa-arrow-alt-circle-down { + --fa: "\f358"; +} + +.fa-arrows-turn-to-dots { + --fa: "\e4c1"; +} + +.fa-florin-sign { + --fa: "\e184"; +} + +.fa-arrow-down-short-wide { + --fa: "\f884"; +} + +.fa-sort-amount-desc { + --fa: "\f884"; +} + +.fa-sort-amount-down-alt { + --fa: "\f884"; +} + +.fa-less-than { + --fa: "\3c"; +} + +.fa-angle-down { + --fa: "\f107"; +} + +.fa-car-tunnel { + --fa: "\e4de"; +} + +.fa-head-side-cough { + --fa: "\e061"; +} + +.fa-grip-lines { + --fa: "\f7a4"; +} + +.fa-thumbs-down { + --fa: "\f165"; +} + +.fa-user-lock { + --fa: "\f502"; +} + +.fa-arrow-right-long { + --fa: "\f178"; +} + +.fa-long-arrow-right { + --fa: "\f178"; +} + +.fa-anchor-circle-xmark { + --fa: "\e4ac"; +} + +.fa-ellipsis { + --fa: "\f141"; +} + +.fa-ellipsis-h { + --fa: "\f141"; +} + +.fa-chess-pawn { + --fa: "\f443"; +} + +.fa-kit-medical { + --fa: "\f479"; +} + +.fa-first-aid { + --fa: "\f479"; +} + +.fa-person-through-window { + --fa: "\e5a9"; +} + +.fa-toolbox { + --fa: "\f552"; +} + +.fa-hands-holding-circle { + --fa: "\e4fb"; +} + +.fa-bug { + --fa: "\f188"; +} + +.fa-credit-card { + --fa: "\f09d"; +} + +.fa-credit-card-alt { + --fa: "\f09d"; +} + +.fa-car { + --fa: "\f1b9"; +} + +.fa-automobile { + --fa: "\f1b9"; +} + +.fa-hand-holding-hand { + --fa: "\e4f7"; +} + +.fa-book-open-reader { + --fa: "\f5da"; +} + +.fa-book-reader { + --fa: "\f5da"; +} + +.fa-mountain-sun { + --fa: "\e52f"; +} + +.fa-arrows-left-right-to-line { + --fa: "\e4ba"; +} + +.fa-dice-d20 { + --fa: "\f6cf"; +} + +.fa-truck-droplet { + --fa: "\e58c"; +} + +.fa-file-circle-xmark { + --fa: "\e5a1"; +} + +.fa-temperature-arrow-up { + --fa: "\e040"; +} + +.fa-temperature-up { + --fa: "\e040"; +} + +.fa-medal { + --fa: "\f5a2"; +} + +.fa-bed { + --fa: "\f236"; +} + +.fa-square-h { + --fa: "\f0fd"; +} + +.fa-h-square { + --fa: "\f0fd"; +} + +.fa-podcast { + --fa: "\f2ce"; +} + +.fa-temperature-full { + --fa: "\f2c7"; +} + +.fa-temperature-4 { + --fa: "\f2c7"; +} + +.fa-thermometer-4 { + --fa: "\f2c7"; +} + +.fa-thermometer-full { + --fa: "\f2c7"; +} + +.fa-bell { + --fa: "\f0f3"; +} + +.fa-superscript { + --fa: "\f12b"; +} + +.fa-plug-circle-xmark { + --fa: "\e560"; +} + +.fa-star-of-life { + --fa: "\f621"; +} + +.fa-phone-slash { + --fa: "\f3dd"; +} + +.fa-paint-roller { + --fa: "\f5aa"; +} + +.fa-handshake-angle { + --fa: "\f4c4"; +} + +.fa-hands-helping { + --fa: "\f4c4"; +} + +.fa-location-dot { + --fa: "\f3c5"; +} + +.fa-map-marker-alt { + --fa: "\f3c5"; +} + +.fa-file { + --fa: "\f15b"; +} + +.fa-greater-than { + --fa: "\3e"; +} + +.fa-person-swimming { + --fa: "\f5c4"; +} + +.fa-swimmer { + --fa: "\f5c4"; +} + +.fa-arrow-down { + --fa: "\f063"; +} + +.fa-droplet { + --fa: "\f043"; +} + +.fa-tint { + --fa: "\f043"; +} + +.fa-eraser { + --fa: "\f12d"; +} + +.fa-earth-americas { + --fa: "\f57d"; +} + +.fa-earth { + --fa: "\f57d"; +} + +.fa-earth-america { + --fa: "\f57d"; +} + +.fa-globe-americas { + --fa: "\f57d"; +} + +.fa-person-burst { + --fa: "\e53b"; +} + +.fa-dove { + --fa: "\f4ba"; +} + +.fa-battery-empty { + --fa: "\f244"; +} + +.fa-battery-0 { + --fa: "\f244"; +} + +.fa-socks { + --fa: "\f696"; +} + +.fa-inbox { + --fa: "\f01c"; +} + +.fa-section { + --fa: "\e447"; +} + +.fa-gauge-high { + --fa: "\f625"; +} + +.fa-tachometer-alt { + --fa: "\f625"; +} + +.fa-tachometer-alt-fast { + --fa: "\f625"; +} + +.fa-envelope-open-text { + --fa: "\f658"; +} + +.fa-hospital { + --fa: "\f0f8"; +} + +.fa-hospital-alt { + --fa: "\f0f8"; +} + +.fa-hospital-wide { + --fa: "\f0f8"; +} + +.fa-wine-bottle { + --fa: "\f72f"; +} + +.fa-chess-rook { + --fa: "\f447"; +} + +.fa-bars-staggered { + --fa: "\f550"; +} + +.fa-reorder { + --fa: "\f550"; +} + +.fa-stream { + --fa: "\f550"; +} + +.fa-dharmachakra { + --fa: "\f655"; +} + +.fa-hotdog { + --fa: "\f80f"; +} + +.fa-person-walking-with-cane { + --fa: "\f29d"; +} + +.fa-blind { + --fa: "\f29d"; +} + +.fa-drum { + --fa: "\f569"; +} + +.fa-ice-cream { + --fa: "\f810"; +} + +.fa-heart-circle-bolt { + --fa: "\e4fc"; +} + +.fa-fax { + --fa: "\f1ac"; +} + +.fa-paragraph { + --fa: "\f1dd"; +} + +.fa-check-to-slot { + --fa: "\f772"; +} + +.fa-vote-yea { + --fa: "\f772"; +} + +.fa-star-half { + --fa: "\f089"; +} + +.fa-boxes-stacked { + --fa: "\f468"; +} + +.fa-boxes { + --fa: "\f468"; +} + +.fa-boxes-alt { + --fa: "\f468"; +} + +.fa-link { + --fa: "\f0c1"; +} + +.fa-chain { + --fa: "\f0c1"; +} + +.fa-ear-listen { + --fa: "\f2a2"; +} + +.fa-assistive-listening-systems { + --fa: "\f2a2"; +} + +.fa-tree-city { + --fa: "\e587"; +} + +.fa-play { + --fa: "\f04b"; +} + +.fa-font { + --fa: "\f031"; +} + +.fa-table-cells-row-lock { + --fa: "\e67a"; +} + +.fa-rupiah-sign { + --fa: "\e23d"; +} + +.fa-magnifying-glass { + --fa: "\f002"; +} + +.fa-search { + --fa: "\f002"; +} + +.fa-table-tennis-paddle-ball { + --fa: "\f45d"; +} + +.fa-ping-pong-paddle-ball { + --fa: "\f45d"; +} + +.fa-table-tennis { + --fa: "\f45d"; +} + +.fa-person-dots-from-line { + --fa: "\f470"; +} + +.fa-diagnoses { + --fa: "\f470"; +} + +.fa-trash-can-arrow-up { + --fa: "\f82a"; +} + +.fa-trash-restore-alt { + --fa: "\f82a"; +} + +.fa-naira-sign { + --fa: "\e1f6"; +} + +.fa-cart-arrow-down { + --fa: "\f218"; +} + +.fa-walkie-talkie { + --fa: "\f8ef"; +} + +.fa-file-pen { + --fa: "\f31c"; +} + +.fa-file-edit { + --fa: "\f31c"; +} + +.fa-receipt { + --fa: "\f543"; +} + +.fa-square-pen { + --fa: "\f14b"; +} + +.fa-pen-square { + --fa: "\f14b"; +} + +.fa-pencil-square { + --fa: "\f14b"; +} + +.fa-suitcase-rolling { + --fa: "\f5c1"; +} + +.fa-person-circle-exclamation { + --fa: "\e53f"; +} + +.fa-chevron-down { + --fa: "\f078"; +} + +.fa-battery-full { + --fa: "\f240"; +} + +.fa-battery { + --fa: "\f240"; +} + +.fa-battery-5 { + --fa: "\f240"; +} + +.fa-skull-crossbones { + --fa: "\f714"; +} + +.fa-code-compare { + --fa: "\e13a"; +} + +.fa-list-ul { + --fa: "\f0ca"; +} + +.fa-list-dots { + --fa: "\f0ca"; +} + +.fa-school-lock { + --fa: "\e56f"; +} + +.fa-tower-cell { + --fa: "\e585"; +} + +.fa-down-long { + --fa: "\f309"; +} + +.fa-long-arrow-alt-down { + --fa: "\f309"; +} + +.fa-ranking-star { + --fa: "\e561"; +} + +.fa-chess-king { + --fa: "\f43f"; +} + +.fa-person-harassing { + --fa: "\e549"; +} + +.fa-brazilian-real-sign { + --fa: "\e46c"; +} + +.fa-landmark-dome { + --fa: "\f752"; +} + +.fa-landmark-alt { + --fa: "\f752"; +} + +.fa-arrow-up { + --fa: "\f062"; +} + +.fa-tv { + --fa: "\f26c"; +} + +.fa-television { + --fa: "\f26c"; +} + +.fa-tv-alt { + --fa: "\f26c"; +} + +.fa-shrimp { + --fa: "\e448"; +} + +.fa-list-check { + --fa: "\f0ae"; +} + +.fa-tasks { + --fa: "\f0ae"; +} + +.fa-jug-detergent { + --fa: "\e519"; +} + +.fa-circle-user { + --fa: "\f2bd"; +} + +.fa-user-circle { + --fa: "\f2bd"; +} + +.fa-user-shield { + --fa: "\f505"; +} + +.fa-wind { + --fa: "\f72e"; +} + +.fa-car-burst { + --fa: "\f5e1"; +} + +.fa-car-crash { + --fa: "\f5e1"; +} + +.fa-y { + --fa: "\59"; +} + +.fa-person-snowboarding { + --fa: "\f7ce"; +} + +.fa-snowboarding { + --fa: "\f7ce"; +} + +.fa-truck-fast { + --fa: "\f48b"; +} + +.fa-shipping-fast { + --fa: "\f48b"; +} + +.fa-fish { + --fa: "\f578"; +} + +.fa-user-graduate { + --fa: "\f501"; +} + +.fa-circle-half-stroke { + --fa: "\f042"; +} + +.fa-adjust { + --fa: "\f042"; +} + +.fa-clapperboard { + --fa: "\e131"; +} + +.fa-circle-radiation { + --fa: "\f7ba"; +} + +.fa-radiation-alt { + --fa: "\f7ba"; +} + +.fa-baseball { + --fa: "\f433"; +} + +.fa-baseball-ball { + --fa: "\f433"; +} + +.fa-jet-fighter-up { + --fa: "\e518"; +} + +.fa-diagram-project { + --fa: "\f542"; +} + +.fa-project-diagram { + --fa: "\f542"; +} + +.fa-copy { + --fa: "\f0c5"; +} + +.fa-volume-xmark { + --fa: "\f6a9"; +} + +.fa-volume-mute { + --fa: "\f6a9"; +} + +.fa-volume-times { + --fa: "\f6a9"; +} + +.fa-hand-sparkles { + --fa: "\e05d"; +} + +.fa-grip { + --fa: "\f58d"; +} + +.fa-grip-horizontal { + --fa: "\f58d"; +} + +.fa-share-from-square { + --fa: "\f14d"; +} + +.fa-share-square { + --fa: "\f14d"; +} + +.fa-child-combatant { + --fa: "\e4e0"; +} + +.fa-child-rifle { + --fa: "\e4e0"; +} + +.fa-gun { + --fa: "\e19b"; +} + +.fa-square-phone { + --fa: "\f098"; +} + +.fa-phone-square { + --fa: "\f098"; +} + +.fa-plus { + --fa: "\2b"; +} + +.fa-add { + --fa: "\2b"; +} + +.fa-expand { + --fa: "\f065"; +} + +.fa-computer { + --fa: "\e4e5"; +} + +.fa-xmark { + --fa: "\f00d"; +} + +.fa-close { + --fa: "\f00d"; +} + +.fa-multiply { + --fa: "\f00d"; +} + +.fa-remove { + --fa: "\f00d"; +} + +.fa-times { + --fa: "\f00d"; +} + +.fa-arrows-up-down-left-right { + --fa: "\f047"; +} + +.fa-arrows { + --fa: "\f047"; +} + +.fa-chalkboard-user { + --fa: "\f51c"; +} + +.fa-chalkboard-teacher { + --fa: "\f51c"; +} + +.fa-peso-sign { + --fa: "\e222"; +} + +.fa-building-shield { + --fa: "\e4d8"; +} + +.fa-baby { + --fa: "\f77c"; +} + +.fa-users-line { + --fa: "\e592"; +} + +.fa-quote-left { + --fa: "\f10d"; +} + +.fa-quote-left-alt { + --fa: "\f10d"; +} + +.fa-tractor { + --fa: "\f722"; +} + +.fa-trash-arrow-up { + --fa: "\f829"; +} + +.fa-trash-restore { + --fa: "\f829"; +} + +.fa-arrow-down-up-lock { + --fa: "\e4b0"; +} + +.fa-lines-leaning { + --fa: "\e51e"; +} + +.fa-ruler-combined { + --fa: "\f546"; +} + +.fa-copyright { + --fa: "\f1f9"; +} + +.fa-equals { + --fa: "\3d"; +} + +.fa-blender { + --fa: "\f517"; +} + +.fa-teeth { + --fa: "\f62e"; +} + +.fa-shekel-sign { + --fa: "\f20b"; +} + +.fa-ils { + --fa: "\f20b"; +} + +.fa-shekel { + --fa: "\f20b"; +} + +.fa-sheqel { + --fa: "\f20b"; +} + +.fa-sheqel-sign { + --fa: "\f20b"; +} + +.fa-map { + --fa: "\f279"; +} + +.fa-rocket { + --fa: "\f135"; +} + +.fa-photo-film { + --fa: "\f87c"; +} + +.fa-photo-video { + --fa: "\f87c"; +} + +.fa-folder-minus { + --fa: "\f65d"; +} + +.fa-hexagon-nodes-bolt { + --fa: "\e69a"; +} + +.fa-store { + --fa: "\f54e"; +} + +.fa-arrow-trend-up { + --fa: "\e098"; +} + +.fa-plug-circle-minus { + --fa: "\e55e"; +} + +.fa-sign-hanging { + --fa: "\f4d9"; +} + +.fa-sign { + --fa: "\f4d9"; +} + +.fa-bezier-curve { + --fa: "\f55b"; +} + +.fa-bell-slash { + --fa: "\f1f6"; +} + +.fa-tablet { + --fa: "\f3fb"; +} + +.fa-tablet-android { + --fa: "\f3fb"; +} + +.fa-school-flag { + --fa: "\e56e"; +} + +.fa-fill { + --fa: "\f575"; +} + +.fa-angle-up { + --fa: "\f106"; +} + +.fa-drumstick-bite { + --fa: "\f6d7"; +} + +.fa-holly-berry { + --fa: "\f7aa"; +} + +.fa-chevron-left { + --fa: "\f053"; +} + +.fa-bacteria { + --fa: "\e059"; +} + +.fa-hand-lizard { + --fa: "\f258"; +} + +.fa-notdef { + --fa: "\e1fe"; +} + +.fa-disease { + --fa: "\f7fa"; +} + +.fa-briefcase-medical { + --fa: "\f469"; +} + +.fa-genderless { + --fa: "\f22d"; +} + +.fa-chevron-right { + --fa: "\f054"; +} + +.fa-retweet { + --fa: "\f079"; +} + +.fa-car-rear { + --fa: "\f5de"; +} + +.fa-car-alt { + --fa: "\f5de"; +} + +.fa-pump-soap { + --fa: "\e06b"; +} + +.fa-video-slash { + --fa: "\f4e2"; +} + +.fa-battery-quarter { + --fa: "\f243"; +} + +.fa-battery-2 { + --fa: "\f243"; +} + +.fa-radio { + --fa: "\f8d7"; +} + +.fa-baby-carriage { + --fa: "\f77d"; +} + +.fa-carriage-baby { + --fa: "\f77d"; +} + +.fa-traffic-light { + --fa: "\f637"; +} + +.fa-thermometer { + --fa: "\f491"; +} + +.fa-vr-cardboard { + --fa: "\f729"; +} + +.fa-hand-middle-finger { + --fa: "\f806"; +} + +.fa-percent { + --fa: "\25"; +} + +.fa-percentage { + --fa: "\25"; +} + +.fa-truck-moving { + --fa: "\f4df"; +} + +.fa-glass-water-droplet { + --fa: "\e4f5"; +} + +.fa-display { + --fa: "\e163"; +} + +.fa-face-smile { + --fa: "\f118"; +} + +.fa-smile { + --fa: "\f118"; +} + +.fa-thumbtack { + --fa: "\f08d"; +} + +.fa-thumb-tack { + --fa: "\f08d"; +} + +.fa-trophy { + --fa: "\f091"; +} + +.fa-person-praying { + --fa: "\f683"; +} + +.fa-pray { + --fa: "\f683"; +} + +.fa-hammer { + --fa: "\f6e3"; +} + +.fa-hand-peace { + --fa: "\f25b"; +} + +.fa-rotate { + --fa: "\f2f1"; +} + +.fa-sync-alt { + --fa: "\f2f1"; +} + +.fa-spinner { + --fa: "\f110"; +} + +.fa-robot { + --fa: "\f544"; +} + +.fa-peace { + --fa: "\f67c"; +} + +.fa-gears { + --fa: "\f085"; +} + +.fa-cogs { + --fa: "\f085"; +} + +.fa-warehouse { + --fa: "\f494"; +} + +.fa-arrow-up-right-dots { + --fa: "\e4b7"; +} + +.fa-splotch { + --fa: "\f5bc"; +} + +.fa-face-grin-hearts { + --fa: "\f584"; +} + +.fa-grin-hearts { + --fa: "\f584"; +} + +.fa-dice-four { + --fa: "\f524"; +} + +.fa-sim-card { + --fa: "\f7c4"; +} + +.fa-transgender { + --fa: "\f225"; +} + +.fa-transgender-alt { + --fa: "\f225"; +} + +.fa-mercury { + --fa: "\f223"; +} + +.fa-arrow-turn-down { + --fa: "\f149"; +} + +.fa-level-down { + --fa: "\f149"; +} + +.fa-person-falling-burst { + --fa: "\e547"; +} + +.fa-award { + --fa: "\f559"; +} + +.fa-ticket-simple { + --fa: "\f3ff"; +} + +.fa-ticket-alt { + --fa: "\f3ff"; +} + +.fa-building { + --fa: "\f1ad"; +} + +.fa-angles-left { + --fa: "\f100"; +} + +.fa-angle-double-left { + --fa: "\f100"; +} + +.fa-qrcode { + --fa: "\f029"; +} + +.fa-clock-rotate-left { + --fa: "\f1da"; +} + +.fa-history { + --fa: "\f1da"; +} + +.fa-face-grin-beam-sweat { + --fa: "\f583"; +} + +.fa-grin-beam-sweat { + --fa: "\f583"; +} + +.fa-file-export { + --fa: "\f56e"; +} + +.fa-arrow-right-from-file { + --fa: "\f56e"; +} + +.fa-shield { + --fa: "\f132"; +} + +.fa-shield-blank { + --fa: "\f132"; +} + +.fa-arrow-up-short-wide { + --fa: "\f885"; +} + +.fa-sort-amount-up-alt { + --fa: "\f885"; +} + +.fa-comment-nodes { + --fa: "\e696"; +} + +.fa-house-medical { + --fa: "\e3b2"; +} + +.fa-golf-ball-tee { + --fa: "\f450"; +} + +.fa-golf-ball { + --fa: "\f450"; +} + +.fa-circle-chevron-left { + --fa: "\f137"; +} + +.fa-chevron-circle-left { + --fa: "\f137"; +} + +.fa-house-chimney-window { + --fa: "\e00d"; +} + +.fa-pen-nib { + --fa: "\f5ad"; +} + +.fa-tent-arrow-turn-left { + --fa: "\e580"; +} + +.fa-tents { + --fa: "\e582"; +} + +.fa-wand-magic { + --fa: "\f0d0"; +} + +.fa-magic { + --fa: "\f0d0"; +} + +.fa-dog { + --fa: "\f6d3"; +} + +.fa-carrot { + --fa: "\f787"; +} + +.fa-moon { + --fa: "\f186"; +} + +.fa-wine-glass-empty { + --fa: "\f5ce"; +} + +.fa-wine-glass-alt { + --fa: "\f5ce"; +} + +.fa-cheese { + --fa: "\f7ef"; +} + +.fa-yin-yang { + --fa: "\f6ad"; +} + +.fa-music { + --fa: "\f001"; +} + +.fa-code-commit { + --fa: "\f386"; +} + +.fa-temperature-low { + --fa: "\f76b"; +} + +.fa-person-biking { + --fa: "\f84a"; +} + +.fa-biking { + --fa: "\f84a"; +} + +.fa-broom { + --fa: "\f51a"; +} + +.fa-shield-heart { + --fa: "\e574"; +} + +.fa-gopuram { + --fa: "\f664"; +} + +.fa-earth-oceania { + --fa: "\e47b"; +} + +.fa-globe-oceania { + --fa: "\e47b"; +} + +.fa-square-xmark { + --fa: "\f2d3"; +} + +.fa-times-square { + --fa: "\f2d3"; +} + +.fa-xmark-square { + --fa: "\f2d3"; +} + +.fa-hashtag { + --fa: "\23"; +} + +.fa-up-right-and-down-left-from-center { + --fa: "\f424"; +} + +.fa-expand-alt { + --fa: "\f424"; +} + +.fa-oil-can { + --fa: "\f613"; +} + +.fa-t { + --fa: "\54"; +} + +.fa-hippo { + --fa: "\f6ed"; +} + +.fa-chart-column { + --fa: "\e0e3"; +} + +.fa-infinity { + --fa: "\f534"; +} + +.fa-vial-circle-check { + --fa: "\e596"; +} + +.fa-person-arrow-down-to-line { + --fa: "\e538"; +} + +.fa-voicemail { + --fa: "\f897"; +} + +.fa-fan { + --fa: "\f863"; +} + +.fa-person-walking-luggage { + --fa: "\e554"; +} + +.fa-up-down { + --fa: "\f338"; +} + +.fa-arrows-alt-v { + --fa: "\f338"; +} + +.fa-cloud-moon-rain { + --fa: "\f73c"; +} + +.fa-calendar { + --fa: "\f133"; +} + +.fa-trailer { + --fa: "\e041"; +} + +.fa-bahai { + --fa: "\f666"; +} + +.fa-haykal { + --fa: "\f666"; +} + +.fa-sd-card { + --fa: "\f7c2"; +} + +.fa-dragon { + --fa: "\f6d5"; +} + +.fa-shoe-prints { + --fa: "\f54b"; +} + +.fa-circle-plus { + --fa: "\f055"; +} + +.fa-plus-circle { + --fa: "\f055"; +} + +.fa-face-grin-tongue-wink { + --fa: "\f58b"; +} + +.fa-grin-tongue-wink { + --fa: "\f58b"; +} + +.fa-hand-holding { + --fa: "\f4bd"; +} + +.fa-plug-circle-exclamation { + --fa: "\e55d"; +} + +.fa-link-slash { + --fa: "\f127"; +} + +.fa-chain-broken { + --fa: "\f127"; +} + +.fa-chain-slash { + --fa: "\f127"; +} + +.fa-unlink { + --fa: "\f127"; +} + +.fa-clone { + --fa: "\f24d"; +} + +.fa-person-walking-arrow-loop-left { + --fa: "\e551"; +} + +.fa-arrow-up-z-a { + --fa: "\f882"; +} + +.fa-sort-alpha-up-alt { + --fa: "\f882"; +} + +.fa-fire-flame-curved { + --fa: "\f7e4"; +} + +.fa-fire-alt { + --fa: "\f7e4"; +} + +.fa-tornado { + --fa: "\f76f"; +} + +.fa-file-circle-plus { + --fa: "\e494"; +} + +.fa-book-quran { + --fa: "\f687"; +} + +.fa-quran { + --fa: "\f687"; +} + +.fa-anchor { + --fa: "\f13d"; +} + +.fa-border-all { + --fa: "\f84c"; +} + +.fa-face-angry { + --fa: "\f556"; +} + +.fa-angry { + --fa: "\f556"; +} + +.fa-cookie-bite { + --fa: "\f564"; +} + +.fa-arrow-trend-down { + --fa: "\e097"; +} + +.fa-rss { + --fa: "\f09e"; +} + +.fa-feed { + --fa: "\f09e"; +} + +.fa-draw-polygon { + --fa: "\f5ee"; +} + +.fa-scale-balanced { + --fa: "\f24e"; +} + +.fa-balance-scale { + --fa: "\f24e"; +} + +.fa-gauge-simple-high { + --fa: "\f62a"; +} + +.fa-tachometer { + --fa: "\f62a"; +} + +.fa-tachometer-fast { + --fa: "\f62a"; +} + +.fa-shower { + --fa: "\f2cc"; +} + +.fa-desktop { + --fa: "\f390"; +} + +.fa-desktop-alt { + --fa: "\f390"; +} + +.fa-m { + --fa: "\4d"; +} + +.fa-table-list { + --fa: "\f00b"; +} + +.fa-th-list { + --fa: "\f00b"; +} + +.fa-comment-sms { + --fa: "\f7cd"; +} + +.fa-sms { + --fa: "\f7cd"; +} + +.fa-book { + --fa: "\f02d"; +} + +.fa-user-plus { + --fa: "\f234"; +} + +.fa-check { + --fa: "\f00c"; +} + +.fa-battery-three-quarters { + --fa: "\f241"; +} + +.fa-battery-4 { + --fa: "\f241"; +} + +.fa-house-circle-check { + --fa: "\e509"; +} + +.fa-angle-left { + --fa: "\f104"; +} + +.fa-diagram-successor { + --fa: "\e47a"; +} + +.fa-truck-arrow-right { + --fa: "\e58b"; +} + +.fa-arrows-split-up-and-left { + --fa: "\e4bc"; +} + +.fa-hand-fist { + --fa: "\f6de"; +} + +.fa-fist-raised { + --fa: "\f6de"; +} + +.fa-cloud-moon { + --fa: "\f6c3"; +} + +.fa-briefcase { + --fa: "\f0b1"; +} + +.fa-person-falling { + --fa: "\e546"; +} + +.fa-image-portrait { + --fa: "\f3e0"; +} + +.fa-portrait { + --fa: "\f3e0"; +} + +.fa-user-tag { + --fa: "\f507"; +} + +.fa-rug { + --fa: "\e569"; +} + +.fa-earth-europe { + --fa: "\f7a2"; +} + +.fa-globe-europe { + --fa: "\f7a2"; +} + +.fa-cart-flatbed-suitcase { + --fa: "\f59d"; +} + +.fa-luggage-cart { + --fa: "\f59d"; +} + +.fa-rectangle-xmark { + --fa: "\f410"; +} + +.fa-rectangle-times { + --fa: "\f410"; +} + +.fa-times-rectangle { + --fa: "\f410"; +} + +.fa-window-close { + --fa: "\f410"; +} + +.fa-baht-sign { + --fa: "\e0ac"; +} + +.fa-book-open { + --fa: "\f518"; +} + +.fa-book-journal-whills { + --fa: "\f66a"; +} + +.fa-journal-whills { + --fa: "\f66a"; +} + +.fa-handcuffs { + --fa: "\e4f8"; +} + +.fa-triangle-exclamation { + --fa: "\f071"; +} + +.fa-exclamation-triangle { + --fa: "\f071"; +} + +.fa-warning { + --fa: "\f071"; +} + +.fa-database { + --fa: "\f1c0"; +} + +.fa-share { + --fa: "\f064"; +} + +.fa-mail-forward { + --fa: "\f064"; +} + +.fa-bottle-droplet { + --fa: "\e4c4"; +} + +.fa-mask-face { + --fa: "\e1d7"; +} + +.fa-hill-rockslide { + --fa: "\e508"; +} + +.fa-right-left { + --fa: "\f362"; +} + +.fa-exchange-alt { + --fa: "\f362"; +} + +.fa-paper-plane { + --fa: "\f1d8"; +} + +.fa-road-circle-exclamation { + --fa: "\e565"; +} + +.fa-dungeon { + --fa: "\f6d9"; +} + +.fa-align-right { + --fa: "\f038"; +} + +.fa-money-bill-1-wave { + --fa: "\f53b"; +} + +.fa-money-bill-wave-alt { + --fa: "\f53b"; +} + +.fa-life-ring { + --fa: "\f1cd"; +} + +.fa-hands { + --fa: "\f2a7"; +} + +.fa-sign-language { + --fa: "\f2a7"; +} + +.fa-signing { + --fa: "\f2a7"; +} + +.fa-calendar-day { + --fa: "\f783"; +} + +.fa-water-ladder { + --fa: "\f5c5"; +} + +.fa-ladder-water { + --fa: "\f5c5"; +} + +.fa-swimming-pool { + --fa: "\f5c5"; +} + +.fa-arrows-up-down { + --fa: "\f07d"; +} + +.fa-arrows-v { + --fa: "\f07d"; +} + +.fa-face-grimace { + --fa: "\f57f"; +} + +.fa-grimace { + --fa: "\f57f"; +} + +.fa-wheelchair-move { + --fa: "\e2ce"; +} + +.fa-wheelchair-alt { + --fa: "\e2ce"; +} + +.fa-turn-down { + --fa: "\f3be"; +} + +.fa-level-down-alt { + --fa: "\f3be"; +} + +.fa-person-walking-arrow-right { + --fa: "\e552"; +} + +.fa-square-envelope { + --fa: "\f199"; +} + +.fa-envelope-square { + --fa: "\f199"; +} + +.fa-dice { + --fa: "\f522"; +} + +.fa-bowling-ball { + --fa: "\f436"; +} + +.fa-brain { + --fa: "\f5dc"; +} + +.fa-bandage { + --fa: "\f462"; +} + +.fa-band-aid { + --fa: "\f462"; +} + +.fa-calendar-minus { + --fa: "\f272"; +} + +.fa-circle-xmark { + --fa: "\f057"; +} + +.fa-times-circle { + --fa: "\f057"; +} + +.fa-xmark-circle { + --fa: "\f057"; +} + +.fa-gifts { + --fa: "\f79c"; +} + +.fa-hotel { + --fa: "\f594"; +} + +.fa-earth-asia { + --fa: "\f57e"; +} + +.fa-globe-asia { + --fa: "\f57e"; +} + +.fa-id-card-clip { + --fa: "\f47f"; +} + +.fa-id-card-alt { + --fa: "\f47f"; +} + +.fa-magnifying-glass-plus { + --fa: "\f00e"; +} + +.fa-search-plus { + --fa: "\f00e"; +} + +.fa-thumbs-up { + --fa: "\f164"; +} + +.fa-user-clock { + --fa: "\f4fd"; +} + +.fa-hand-dots { + --fa: "\f461"; +} + +.fa-allergies { + --fa: "\f461"; +} + +.fa-file-invoice { + --fa: "\f570"; +} + +.fa-window-minimize { + --fa: "\f2d1"; +} + +.fa-mug-saucer { + --fa: "\f0f4"; +} + +.fa-coffee { + --fa: "\f0f4"; +} + +.fa-brush { + --fa: "\f55d"; +} + +.fa-file-half-dashed { + --fa: "\e698"; +} + +.fa-mask { + --fa: "\f6fa"; +} + +.fa-magnifying-glass-minus { + --fa: "\f010"; +} + +.fa-search-minus { + --fa: "\f010"; +} + +.fa-ruler-vertical { + --fa: "\f548"; +} + +.fa-user-large { + --fa: "\f406"; +} + +.fa-user-alt { + --fa: "\f406"; +} + +.fa-train-tram { + --fa: "\e5b4"; +} + +.fa-user-nurse { + --fa: "\f82f"; +} + +.fa-syringe { + --fa: "\f48e"; +} + +.fa-cloud-sun { + --fa: "\f6c4"; +} + +.fa-stopwatch-20 { + --fa: "\e06f"; +} + +.fa-square-full { + --fa: "\f45c"; +} + +.fa-magnet { + --fa: "\f076"; +} + +.fa-jar { + --fa: "\e516"; +} + +.fa-note-sticky { + --fa: "\f249"; +} + +.fa-sticky-note { + --fa: "\f249"; +} + +.fa-bug-slash { + --fa: "\e490"; +} + +.fa-arrow-up-from-water-pump { + --fa: "\e4b6"; +} + +.fa-bone { + --fa: "\f5d7"; +} + +.fa-table-cells-row-unlock { + --fa: "\e691"; +} + +.fa-user-injured { + --fa: "\f728"; +} + +.fa-face-sad-tear { + --fa: "\f5b4"; +} + +.fa-sad-tear { + --fa: "\f5b4"; +} + +.fa-plane { + --fa: "\f072"; +} + +.fa-tent-arrows-down { + --fa: "\e581"; +} + +.fa-exclamation { + --fa: "\21"; +} + +.fa-arrows-spin { + --fa: "\e4bb"; +} + +.fa-print { + --fa: "\f02f"; +} + +.fa-turkish-lira-sign { + --fa: "\e2bb"; +} + +.fa-try { + --fa: "\e2bb"; +} + +.fa-turkish-lira { + --fa: "\e2bb"; +} + +.fa-dollar-sign { + --fa: "\24"; +} + +.fa-dollar { + --fa: "\24"; +} + +.fa-usd { + --fa: "\24"; +} + +.fa-x { + --fa: "\58"; +} + +.fa-magnifying-glass-dollar { + --fa: "\f688"; +} + +.fa-search-dollar { + --fa: "\f688"; +} + +.fa-users-gear { + --fa: "\f509"; +} + +.fa-users-cog { + --fa: "\f509"; +} + +.fa-person-military-pointing { + --fa: "\e54a"; +} + +.fa-building-columns { + --fa: "\f19c"; +} + +.fa-bank { + --fa: "\f19c"; +} + +.fa-institution { + --fa: "\f19c"; +} + +.fa-museum { + --fa: "\f19c"; +} + +.fa-university { + --fa: "\f19c"; +} + +.fa-umbrella { + --fa: "\f0e9"; +} + +.fa-trowel { + --fa: "\e589"; +} + +.fa-d { + --fa: "\44"; +} + +.fa-stapler { + --fa: "\e5af"; +} + +.fa-masks-theater { + --fa: "\f630"; +} + +.fa-theater-masks { + --fa: "\f630"; +} + +.fa-kip-sign { + --fa: "\e1c4"; +} + +.fa-hand-point-left { + --fa: "\f0a5"; +} + +.fa-handshake-simple { + --fa: "\f4c6"; +} + +.fa-handshake-alt { + --fa: "\f4c6"; +} + +.fa-jet-fighter { + --fa: "\f0fb"; +} + +.fa-fighter-jet { + --fa: "\f0fb"; +} + +.fa-square-share-nodes { + --fa: "\f1e1"; +} + +.fa-share-alt-square { + --fa: "\f1e1"; +} + +.fa-barcode { + --fa: "\f02a"; +} + +.fa-plus-minus { + --fa: "\e43c"; +} + +.fa-video { + --fa: "\f03d"; +} + +.fa-video-camera { + --fa: "\f03d"; +} + +.fa-graduation-cap { + --fa: "\f19d"; +} + +.fa-mortar-board { + --fa: "\f19d"; +} + +.fa-hand-holding-medical { + --fa: "\e05c"; +} + +.fa-person-circle-check { + --fa: "\e53e"; +} + +.fa-turn-up { + --fa: "\f3bf"; +} + +.fa-level-up-alt { + --fa: "\f3bf"; +} + +.sr-only, +.fa-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.sr-only-focusable:not(:focus), +.fa-sr-only-focusable:not(:focus) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} diff --git a/apps/mobile/global.css b/apps/mobile/global.css new file mode 100644 index 0000000..83666a3 --- /dev/null +++ b/apps/mobile/global.css @@ -0,0 +1,5 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +body { + font-family: "Inter", sans-serif !important; +} diff --git a/apps/mobile/global.d.ts b/apps/mobile/global.d.ts new file mode 100644 index 0000000..53c72cf --- /dev/null +++ b/apps/mobile/global.d.ts @@ -0,0 +1,33 @@ +declare module 'react-native/Libraries/Core/ExceptionsManager' { + export function handleException(err: Error, isFatal: boolean): void; +} + +declare module 'react-native-safe-area-context/lib/commonjs' { + export const SafeAreaView: React.ComponentType; + export const SafeAreaProvider: React.ComponentType; + export const SafeAreaInsetsContext: React.Context; + export const SafeAreaFrameContext: React.Context; + export function useSafeAreaInsets(): { top: number; right: number; bottom: number; left: number }; + export function useSafeAreaFrame(): { x: number; y: number; width: number; height: number }; + export const initialWindowMetrics: any; +} + +declare module 'react-native-web-refresh-control' { + export const RefreshControl: React.ComponentType; +} + +declare module 'react-native-web/dist/exports/ScrollView' { + const ScrollView: React.ComponentType; + export default ScrollView; +} + +declare module '@anythingai/app/screens/launcher-menu' { + const LauncherMenuContainer: React.ComponentType; + export default LauncherMenuContainer; +} + +declare module 'lodash' { + export function merge(...args: T[]): T; +} + +declare module '*.css' {} diff --git a/apps/mobile/index.tsx b/apps/mobile/index.tsx new file mode 100644 index 0000000..db6b7ae --- /dev/null +++ b/apps/mobile/index.tsx @@ -0,0 +1,30 @@ +import ExceptionsManager from "react-native/Libraries/Core/ExceptionsManager"; + +if (__DEV__) { + ExceptionsManager.handleException = (_error, _isFatal) => { + // no-op + }; +} + +import "react-native-url-polyfill/auto"; +import "./src/__create/polyfills"; +global.Buffer = require("buffer").Buffer; + +import "@expo/metro-runtime"; +import { AppRegistry, LogBox } from "react-native"; +import { initSentry } from "./__create/sentry"; +import { initTestFlightLogger } from "./__create/testflight-logger"; +import { renderRootComponent } from "expo-router/build/renderRootComponent"; +import App from "./entrypoint"; + +initSentry(); +initTestFlightLogger(); + +if (__DEV__ || process.env.EXPO_PUBLIC_CREATE_ENV === "DEVELOPMENT") { + LogBox.ignoreAllLogs(); + LogBox.uninstall(); + AppRegistry.setWrapperComponentProvider(() => ({ children }) => { + return <>{children}; + }); +} +renderRootComponent(App); diff --git a/apps/mobile/index.web.tsx b/apps/mobile/index.web.tsx new file mode 100644 index 0000000..671bfd9 --- /dev/null +++ b/apps/mobile/index.web.tsx @@ -0,0 +1,127 @@ +import '@expo/metro-runtime'; +import { toPng } from 'html-to-image'; +import React, { useEffect } from 'react'; +import { renderRootComponent } from 'expo-router/build/renderRootComponent'; + +import { LoadSkiaWeb } from '@shopify/react-native-skia/lib/module/web'; +import CreateApp from './App'; +async function inlineGoogleFonts(): Promise { + // Find all elements that load Google Fonts CSS + const links = Array.from(document.querySelectorAll( + 'link[rel="stylesheet"][href*="fonts.googleapis.com"]' + )); + + for (const link of links) { + try { + const href = link.href; + const res = await fetch(href); + let cssText = await res.text(); + + // Ensure font URLs are absolute + cssText = cssText.replace(/url\(([^)]+)\)/g, (match, url) => { + const clean = url.replace(/["']/g, ""); + if (clean.startsWith("http")) { + return `url(${clean})`; + } + return `url(${new URL(clean, href).toString()})`; + }); + + // Inject