Merge branch 'master' into unit-tests

This commit is contained in:
Brandon Presley 2023-01-01 13:57:01 +13:00
commit e43188ccdf
22 changed files with 207 additions and 169 deletions

View File

@ -1,10 +1,10 @@
import {ComponentProps, Ref} from 'react' import React, {ComponentProps, Ref} from 'react'
import {TextInput} from 'react-native-paper' import {TextInput} from 'react-native-paper'
import {CombinedDefaultTheme} from './App' import {CombinedDefaultTheme} from './App'
import {MARGIN} from './constants' import {MARGIN} from './constants'
import useDark from './use-dark' import useDark from './use-dark'
export default function AppInput( function AppInput(
props: Partial<ComponentProps<typeof TextInput>> & { props: Partial<ComponentProps<typeof TextInput>> & {
innerRef?: Ref<any> innerRef?: Ref<any>
}, },
@ -14,7 +14,6 @@ export default function AppInput(
return ( return (
<TextInput <TextInput
selectionColor={dark ? '#2A2A2A' : CombinedDefaultTheme.colors.border} selectionColor={dark ? '#2A2A2A' : CombinedDefaultTheme.colors.border}
mode="outlined"
style={{marginBottom: MARGIN, minWidth: 100}} style={{marginBottom: MARGIN, minWidth: 100}}
selectTextOnFocus selectTextOnFocus
ref={props.innerRef} ref={props.innerRef}
@ -23,3 +22,5 @@ export default function AppInput(
/> />
) )
} }
export default React.memo(AppInput)

View File

@ -80,9 +80,9 @@ export default function EditPlan() {
<Switch <Switch
key={day} key={day}
onChange={value => toggleDay(value, day)} onChange={value => toggleDay(value, day)}
value={days.includes(day)}> value={days.includes(day)}
{day} title={day}
</Switch> />
))} ))}
<Text style={[styles.title, {marginTop: MARGIN}]}>Workouts</Text> <Text style={[styles.title, {marginTop: MARGIN}]}>Workouts</Text>
{names.length === 0 ? ( {names.length === 0 ? (
@ -94,9 +94,9 @@ export default function EditPlan() {
<Switch <Switch
key={name} key={name}
onChange={value => toggleWorkout(value, name)} onChange={value => toggleWorkout(value, name)}
value={workouts.includes(name)}> value={workouts.includes(name)}
{name} title={name}
</Switch> />
)) ))
)} )}
</ScrollView> </ScrollView>

View File

@ -162,7 +162,7 @@ export default function EditSet() {
<AppInput <AppInput
label="Created" label="Created"
disabled disabled
value={format(new Date(set.created), settings.date)} value={format(new Date(set.created), settings.date || 'P')}
/> />
)} )}

View File

@ -44,7 +44,6 @@ export default function ListMenu({
} }
const select = () => { const select = () => {
setShowMenu(false)
onSelect() onSelect()
} }

Binary file not shown.

View File

@ -1,6 +1,5 @@
import {createDrawerNavigator} from '@react-navigation/drawer' import {createDrawerNavigator} from '@react-navigation/drawer'
import {useMemo} from 'react' import {useMemo} from 'react'
import {Platform} from 'react-native'
import {IconButton} from 'react-native-paper' import {IconButton} from 'react-native-paper'
import BestPage from './BestPage' import BestPage from './BestPage'
import {DrawerParamList} from './drawer-param-list' import {DrawerParamList} from './drawer-param-list'
@ -36,22 +35,16 @@ export default function Routes() {
swipeEdgeWidth: 1000, swipeEdgeWidth: 1000,
headerShown: false, headerShown: false,
}}> }}>
{} {routes.map(route => (
{routes <Drawer.Screen
.filter(route => { key={route.name}
if (Platform.OS === 'ios' && route.name === 'Timer') return false name={route.name}
return true component={route.component}
}) options={{
.map(route => ( drawerIcon: () => <IconButton icon={route.icon} />,
<Drawer.Screen }}
key={route.name} />
name={route.name} ))}
component={route.component}
options={{
drawerIcon: () => <IconButton icon={route.icon} />,
}}
/>
))}
</Drawer.Navigator> </Drawer.Navigator>
) )
} }

View File

@ -1,4 +1,4 @@
import {useCallback, useMemo, useState} from 'react' import React, {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Button, Menu, Subheading, useTheme} from 'react-native-paper' import {Button, Menu, Subheading, useTheme} from 'react-native-paper'
import {ITEM_PADDING} from './constants' import {ITEM_PADDING} from './constants'
@ -9,7 +9,7 @@ export interface Item {
color?: string color?: string
} }
export default function Select({ function Select({
value, value,
onChange, onChange,
items, items,
@ -68,3 +68,5 @@ export default function Select({
</View> </View>
) )
} }
export default React.memo(Select)

View File

@ -19,28 +19,32 @@ import Settings from './settings'
import Switch from './Switch' import Switch from './Switch'
import {toast} from './toast' import {toast} from './toast'
import {useTheme} from './use-theme' import {useTheme} from './use-theme'
import {useForm} from 'react-hook-form'
const defaultFormats = ['P', 'Pp', 'ccc p', 'p'] const twelveHours = ['P', 'Pp', 'ccc p', 'p', 'yyyy-MM-d', 'yyyy.MM.d']
const twentyFours = ['P', 'P, k:m', 'ccc k:m', 'k:m', 'yyyy-MM-d', 'yyyy.MM.d']
export default function SettingsPage() { export default function SettingsPage() {
const [ignoring, setIgnoring] = useState(false) const [ignoring, setIgnoring] = useState(false)
const [term, setTerm] = useState('') const [term, setTerm] = useState('')
const [formatOptions, setFormatOptions] = useState<string[]>(defaultFormats) const [formatOptions, setFormatOptions] = useState<string[]>(twelveHours)
const [importing, setImporting] = useState(false) const [importing, setImporting] = useState(false)
const [settings, setSettings] = useState(new Settings())
const {reset} = useNavigation<NavigationProp<DrawerParamList>>() const {reset} = useNavigation<NavigationProp<DrawerParamList>>()
const today = new Date()
const {watch, setValue} = useForm<Settings>({
defaultValues: () => settingsRepo.findOne({where: {}}),
})
const settings = watch()
const {theme, setTheme, lightColor, setLightColor, darkColor, setDarkColor} = const {theme, setTheme, lightColor, setLightColor, darkColor, setDarkColor} =
useTheme() useTheme()
useEffect(() => { useEffect(() => {
settingsRepo.findOne({where: {}}).then(setSettings)
NativeModules.SettingsModule.ignoringBattery(setIgnoring) NativeModules.SettingsModule.ignoringBattery(setIgnoring)
NativeModules.SettingsModule.is24().then((is24: boolean) => { NativeModules.SettingsModule.is24().then((is24: boolean) => {
console.log(`${SettingsPage.name}.focus:`, {is24}) console.log(`${SettingsPage.name}.focus:`, {is24})
if (is24) setFormatOptions(['P', 'P, k:m', 'ccc k:m', 'k:m']) if (is24) setFormatOptions(twentyFours)
else setFormatOptions(defaultFormats) else setFormatOptions(twelveHours)
}) })
}, []) }, [])
@ -56,54 +60,34 @@ export default function SettingsPage() {
copyTo: 'documentDirectory', copyTo: 'documentDirectory',
}) })
if (!fileCopyUri) return if (!fileCopyUri) return
const updated = await settingsRepo.save({...settings, sound: fileCopyUri}) setValue('sound', fileCopyUri)
setSettings(updated) await settingsRepo.save({...settings, sound: fileCopyUri})
toast('Sound will play after rest timers.') toast('Sound will play after rest timers.')
}, [settings]) }, [settings, setValue])
const switches: Input<boolean>[] = [ const switches: Input<boolean>[] = useMemo(
{name: 'Rest timers', value: settings.alarm, key: 'alarm'}, () => [
{name: 'Vibrate', value: settings.vibrate, key: 'vibrate'}, {name: 'Rest timers', value: settings.alarm, key: 'alarm'},
{name: 'Disable sound', value: settings.noSound, key: 'noSound'}, {name: 'Vibrate', value: settings.vibrate, key: 'vibrate'},
{name: 'Notifications', value: settings.notify, key: 'notify'}, {name: 'Disable sound', value: settings.noSound, key: 'noSound'},
{name: 'Show images', value: settings.images, key: 'images'}, {name: 'Notifications', value: settings.notify, key: 'notify'},
{name: 'Show unit', value: settings.showUnit, key: 'showUnit'}, {name: 'Show images', value: settings.images, key: 'images'},
{name: 'Show steps', value: settings.steps, key: 'steps'}, {name: 'Show unit', value: settings.showUnit, key: 'showUnit'},
{name: 'Show date', value: settings.showDate, key: 'showDate'}, {name: 'Show steps', value: settings.steps, key: 'steps'},
] {name: 'Show date', value: settings.showDate, key: 'showDate'},
],
[settings],
)
const changeString = useCallback( const filter = useCallback(
async (key: keyof Settings, value: string) => { ({name}) => name.toLowerCase().includes(term.toLowerCase()),
const updated = await settingsRepo.save({...settings, [key]: value}) [term],
setSettings(updated)
switch (key) {
case 'date':
return toast('Changed date format')
case 'darkColor':
setDarkColor(value)
return toast('Set primary color for dark mode.')
case 'lightColor':
setLightColor(value)
return toast('Set primary color for light mode.')
case 'vibrate':
return toast('Set primary color for light mode.')
case 'sound':
return toast('Sound will play after rest timers.')
case 'theme':
setTheme(value as string)
if (value === 'dark') toast('Theme will always be dark.')
else if (value === 'light') toast('Theme will always be light.')
else if (value === 'system') toast('Theme will follow system.')
return
}
},
[settings, setTheme, setDarkColor, setLightColor],
) )
const changeBoolean = useCallback( const changeBoolean = useCallback(
async (key: keyof Settings, value: boolean) => { async (key: keyof Settings, value: boolean) => {
const updated = await settingsRepo.save({...settings, [key]: value}) setValue(key, value)
setSettings(updated) await settingsRepo.save({...settings, [key]: value})
switch (key) { switch (key) {
case 'alarm': case 'alarm':
if (value) toast('Timers will now run after each set.') if (value) toast('Timers will now run after each set.')
@ -140,7 +124,7 @@ export default function SettingsPage() {
return return
} }
}, },
[settings, ignoring], [settings, ignoring, setValue],
) )
const renderSwitch = useCallback( const renderSwitch = useCallback(
@ -148,37 +132,73 @@ export default function SettingsPage() {
<Switch <Switch
key={item.name} key={item.name}
value={item.value} value={item.value}
onChange={value => changeBoolean(item.key, value)}> onChange={value => changeBoolean(item.key, value)}
{item.name} title={item.name}
</Switch> />
), ),
[changeBoolean], [changeBoolean],
) )
const selects: Input<string>[] = [ const switchesMarkup = useMemo(
{name: 'Theme', value: theme, items: themeOptions, key: 'theme'}, () => switches.filter(filter).map(s => renderSwitch(s)),
{ [filter, switches, renderSwitch],
name: 'Dark color', )
value: darkColor,
items: lightOptions, const changeString = useCallback(
key: 'darkColor', async (key: keyof Settings, value: string) => {
setValue(key, value)
await settingsRepo.save({...settings, [key]: value})
switch (key) {
case 'date':
return toast('Changed date format')
case 'darkColor':
setDarkColor(value)
return toast('Set primary color for dark mode.')
case 'lightColor':
setLightColor(value)
return toast('Set primary color for light mode.')
case 'vibrate':
return toast('Set primary color for light mode.')
case 'sound':
return toast('Sound will play after rest timers.')
case 'theme':
setTheme(value as string)
if (value === 'dark') toast('Theme will always be dark.')
else if (value === 'light') toast('Theme will always be light.')
else if (value === 'system') toast('Theme will follow system.')
return
}
}, },
{ [settings, setTheme, setDarkColor, setLightColor, setValue],
name: 'Light color', )
value: lightColor,
items: darkOptions, const selects: Input<string>[] = useMemo(() => {
key: 'lightColor', const today = new Date()
}, return [
{ {name: 'Theme', value: theme, items: themeOptions, key: 'theme'},
name: 'Date format', {
value: settings.date, name: 'Dark color',
items: formatOptions.map(option => ({ value: darkColor,
label: format(today, option), items: lightOptions,
value: option, key: 'darkColor',
})), },
key: 'date', {
}, name: 'Light color',
] value: lightColor,
items: darkOptions,
key: 'lightColor',
},
{
name: 'Date format',
value: settings.date,
items: formatOptions.map(option => ({
label: format(today, option),
value: option,
})),
key: 'date',
},
]
}, [settings.date, darkColor, formatOptions, theme, lightColor])
const renderSelect = useCallback( const renderSelect = useCallback(
(item: Input<string>) => ( (item: Input<string>) => (
@ -193,6 +213,11 @@ export default function SettingsPage() {
[changeString], [changeString],
) )
const selectsMarkup = useMemo(
() => selects.filter(filter).map(renderSelect),
[filter, selects, renderSelect],
)
const confirmImport = useCallback(async () => { const confirmImport = useCallback(async () => {
setImporting(false) setImporting(false)
await AppDataSource.destroy() await AppDataSource.destroy()
@ -215,47 +240,51 @@ export default function SettingsPage() {
}, []) }, [])
const buttons = useMemo( const buttons = useMemo(
() => () => [
[ {
{ name: 'Alarm sound',
name: 'Alarm sound', element: (
element: ( <View
<View key="alarm-sound"
key="alarm-sound" style={{
style={{ flexDirection: 'row',
flexDirection: 'row', alignItems: 'center',
alignItems: 'center', paddingLeft: ITEM_PADDING,
paddingLeft: ITEM_PADDING, }}>
}}> <Subheading style={{width: 100}}>Alarm sound</Subheading>
<Subheading style={{width: 100}}>Alarm sound</Subheading> <Button onPress={changeSound}>{soundString || 'Default'}</Button>
<Button onPress={changeSound}>{soundString || 'Default'}</Button> </View>
</View> ),
), },
}, {
{ name: 'Export database',
name: 'Export database', element: (
element: ( <Button
<Button key="export-db"
key="export-db" style={{alignSelf: 'flex-start'}}
style={{alignSelf: 'flex-start'}} onPress={exportDatabase}>
onPress={exportDatabase}> Export database
Export database </Button>
</Button> ),
), },
}, {
{ name: 'Import database',
name: 'Import database', element: (
element: ( <Button
<Button key="import-db"
key="import-db" style={{alignSelf: 'flex-start'}}
style={{alignSelf: 'flex-start'}} onPress={() => setImporting(true)}>
onPress={() => setImporting(true)}> Import database
Import database </Button>
</Button> ),
), },
}, ],
].filter(({name}) => name.toLowerCase().includes(term.toLowerCase())), [changeSound, exportDatabase, soundString],
[changeSound, exportDatabase, soundString, term], )
const buttonsMarkup = useMemo(
() => buttons.filter(filter).map(b => b.element),
[buttons, filter],
) )
return ( return (
@ -264,9 +293,9 @@ export default function SettingsPage() {
<Page term={term} search={setTerm} style={{flexGrow: 0}}> <Page term={term} search={setTerm} style={{flexGrow: 0}}>
<View style={{marginTop: MARGIN}}> <View style={{marginTop: MARGIN}}>
{switches.map(s => renderSwitch(s))} {switchesMarkup}
{selects.map(s => renderSelect(s))} {selectsMarkup}
{buttons.map(b => b.element)} {buttonsMarkup}
</View> </View>
</Page> </Page>

View File

@ -1,15 +1,16 @@
import React from 'react'
import {Platform, Pressable} from 'react-native' import {Platform, Pressable} from 'react-native'
import {Switch as PaperSwitch, Text, useTheme} from 'react-native-paper' import {Switch as PaperSwitch, Text, useTheme} from 'react-native-paper'
import {MARGIN} from './constants' import {MARGIN} from './constants'
export default function Switch({ function Switch({
value, value,
onChange, onChange,
children, title,
}: { }: {
value?: boolean value?: boolean
onChange: (value: boolean) => void onChange: (value: boolean) => void
children: string title: string
}) { }) {
const {colors} = useTheme() const {colors} = useTheme()
@ -29,7 +30,9 @@ export default function Switch({
onValueChange={onChange} onValueChange={onChange}
trackColor={{true: colors.primary + '80', false: colors.disabled}} trackColor={{true: colors.primary + '80', false: colors.disabled}}
/> />
<Text>{children}</Text> <Text>{title}</Text>
</Pressable> </Pressable>
) )
} }
export default React.memo(Switch)

View File

@ -8,16 +8,16 @@ GEM
artifactory (3.0.15) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.657.0) aws-partitions (1.686.0)
aws-sdk-core (3.166.0) aws-sdk-core (3.168.4)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.59.0) aws-sdk-kms (1.61.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.117.1) aws-sdk-s3 (1.117.2)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1) dotenv (2.8.1)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.93.1) excon (0.95.0)
faraday (1.10.2) faraday (1.10.2)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.6) fastimage (2.2.6)
fastlane (2.210.1) fastlane (2.211.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -106,9 +106,9 @@ GEM
xcpretty (~> 0.3.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.31.0) google-apis-androidpublisher_v3 (0.32.0)
google-apis-core (>= 0.9.1, < 2.a) google-apis-core (>= 0.9.1, < 2.a)
google-apis-core (0.9.1) google-apis-core (0.9.2)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -148,11 +148,11 @@ GEM
http-cookie (1.0.5) http-cookie (1.0.5)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.1) jmespath (1.6.2)
json (2.6.2) json (2.6.3)
jwt (2.5.0) jwt (2.6.0)
memoist (0.16.2) memoist (0.16.2)
mini_magick (4.11.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.0.0) multipart-post (2.0.0)
@ -161,7 +161,7 @@ GEM
optparse (0.1.1) optparse (0.1.1)
os (1.1.4) os (1.1.4)
plist (3.6.0) plist (3.6.0)
public_suffix (5.0.0) public_suffix (5.0.1)
rake (13.0.6) rake (13.0.6)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)

View File

@ -41,8 +41,8 @@ android {
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 36128 versionCode 36133
versionName "1.102" versionName "1.107"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
if (isNewArchitectureEnabled()) { if (isNewArchitectureEnabled()) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@ -1,6 +1,6 @@
{ {
"name": "massive", "name": "massive",
"version": "1.102", "version": "1.107",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
@ -30,6 +30,7 @@
"eslint-plugin-flowtype": "^8.0.3", "eslint-plugin-flowtype": "^8.0.3",
"jest": "^29.2.2", "jest": "^29.2.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-hook-form": "^7.41.2",
"react-native": "^0.70.5", "react-native": "^0.70.5",
"react-native-document-picker": "^8.1.2", "react-native-document-picker": "^8.1.2",
"react-native-file-access": "^2.5.0", "react-native-file-access": "^2.5.0",

View File

@ -7295,6 +7295,7 @@ __metadata:
jest: ^29.2.2 jest: ^29.2.2
metro-react-native-babel-preset: ^0.73.3 metro-react-native-babel-preset: ^0.73.3
react: ^18.2.0 react: ^18.2.0
react-hook-form: ^7.41.2
react-native: ^0.70.5 react-native: ^0.70.5
react-native-document-picker: ^8.1.2 react-native-document-picker: ^8.1.2
react-native-file-access: ^2.5.0 react-native-file-access: ^2.5.0
@ -8752,6 +8753,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-hook-form@npm:^7.41.2":
version: 7.41.2
resolution: "react-hook-form@npm:7.41.2"
peerDependencies:
react: ^16.8.0 || ^17 || ^18
checksum: bc923b74018d55289838f820d49e32043dbc683d97ea2f93a6f3b75ff58fea9ee4536d6487adcb02912b4bc90a09ea07a63c4c24f930ec59f598bdafd5e8c8d3
languageName: node
linkType: hard
"react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0": "react-is@npm:^16.12.0 || ^17.0.0 || ^18.0.0, react-is@npm:^18.0.0, react-is@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react-is@npm:18.2.0" resolution: "react-is@npm:18.2.0"