Improve form styles

This commit is contained in:
thatmattlove 2021-12-06 13:06:01 -07:00
parent 55a9918fd0
commit 7c1a5bf1c3
8 changed files with 162 additions and 126 deletions

View file

@ -1,7 +1,7 @@
"""Validate branding configuration variables.""" """Validate branding configuration variables."""
# Standard Library # Standard Library
from typing import Union, Optional, Sequence import typing as t
from pathlib import Path from pathlib import Path
# Third Party # Third Party
@ -33,13 +33,14 @@ ColorMode = constr(regex=r"light|dark")
DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys())) DOHProvider = constr(regex="|".join(DNS_OVER_HTTPS.keys()))
Title = constr(max_length=32) Title = constr(max_length=32)
Side = constr(regex=r"left|right") Side = constr(regex=r"left|right")
LocationDisplayMode = t.Literal["auto", "dropdown", "gallery"]
class Analytics(HyperglassModel): class Analytics(HyperglassModel):
"""Validation model for Google Analytics.""" """Validation model for Google Analytics."""
enable: StrictBool = False enable: StrictBool = False
id: Optional[StrictStr] id: t.Optional[StrictStr]
@validator("id") @validator("id")
def validate_id(cls, value, values): def validate_id(cls, value, values):
@ -102,7 +103,7 @@ class Greeting(HyperglassModel):
"""Validation model for greeting modal.""" """Validation model for greeting modal."""
enable: StrictBool = False enable: StrictBool = False
file: Optional[FilePath] file: t.Optional[FilePath]
title: StrictStr = "Welcome" title: StrictStr = "Welcome"
button: StrictStr = "Continue" button: StrictStr = "Continue"
required: StrictBool = False required: StrictBool = False
@ -121,8 +122,8 @@ class Logo(HyperglassModel):
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg" light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg" dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg" favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
width: Optional[Union[StrictInt, Percentage]] = "100%" width: t.Optional[t.Union[StrictInt, Percentage]] = "100%"
height: Optional[Union[StrictInt, Percentage]] height: t.Optional[t.Union[StrictInt, Percentage]]
class LogoPublic(Logo): class LogoPublic(Logo):
@ -188,12 +189,12 @@ class ThemeColors(HyperglassModel):
cyan: Color = "#118ab2" cyan: Color = "#118ab2"
pink: Color = "#f2607d" pink: Color = "#f2607d"
purple: Color = "#8d30b5" purple: Color = "#8d30b5"
primary: Optional[Color] primary: t.Optional[Color]
secondary: Optional[Color] secondary: t.Optional[Color]
success: Optional[Color] success: t.Optional[Color]
warning: Optional[Color] warning: t.Optional[Color]
error: Optional[Color] error: t.Optional[Color]
danger: Optional[Color] danger: t.Optional[Color]
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True) @validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True)
def validate_colors(cls, value, values, field): def validate_colors(cls, value, values, field):
@ -226,7 +227,7 @@ class Theme(HyperglassModel):
"""Validation model for theme variables.""" """Validation model for theme variables."""
colors: ThemeColors = ThemeColors() colors: ThemeColors = ThemeColors()
default_color_mode: Optional[ColorMode] default_color_mode: t.Optional[ColorMode]
fonts: ThemeFonts = ThemeFonts() fonts: ThemeFonts = ThemeFonts()
@ -256,10 +257,10 @@ class Web(HyperglassModel):
credit: Credit = Credit() credit: Credit = Credit()
dns_provider: DnsOverHttps = DnsOverHttps() dns_provider: DnsOverHttps = DnsOverHttps()
links: Sequence[Link] = [ links: t.Sequence[Link] = [
Link(title="PeeringDB", url="https://www.peeringdb.com/asn/{primary_asn}") Link(title="PeeringDB", url="https://www.peeringdb.com/asn/{primary_asn}")
] ]
menus: Sequence[Menu] = [ menus: t.Sequence[Menu] = [
Menu(title="Terms", content=DEFAULT_TERMS), Menu(title="Terms", content=DEFAULT_TERMS),
Menu(title="Help", content=DEFAULT_HELP), Menu(title="Help", content=DEFAULT_HELP),
] ]
@ -268,6 +269,7 @@ class Web(HyperglassModel):
opengraph: OpenGraph = OpenGraph() opengraph: OpenGraph = OpenGraph()
text: Text = Text() text: Text = Text()
theme: Theme = Theme() theme: Theme = Theme()
location_display_mode: LocationDisplayMode = "auto"
class WebPublic(Web): class WebPublic(Web):

View file

@ -38,13 +38,14 @@ export const FormField: React.FC<TField> = (props: TField) => {
{...rest} {...rest}
> >
<FormLabel <FormLabel
pl={1}
pr={0} pr={0}
mb={{ lg: 4 }}
htmlFor={name} htmlFor={name}
display="flex" display="flex"
opacity={opacity} opacity={opacity}
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
fontWeight="bold"
color={error !== null ? errorColor : labelColor} color={error !== null ? errorColor : labelColor}
> >
{label} {label}

View file

@ -0,0 +1,99 @@
import { useMemo, useState } from 'react';
import { Flex, Box, Avatar, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { useColorValue } from '~/context';
import { useOpposingColor } from '~/hooks';
import type { LocationOption } from './queryLocation';
import type { LocationCardProps } from './types';
const MotionBox = motion(Box);
export const LocationCard = (props: LocationCardProps): JSX.Element => {
const { option, onChange, defaultChecked, hasError } = props;
const { label } = option;
const [isChecked, setChecked] = useState(defaultChecked);
function handleChange(value: LocationOption) {
if (isChecked) {
setChecked(false);
onChange('remove', value);
} else {
setChecked(true);
onChange('add', value);
}
}
const bg = useColorValue('white', 'blackSolid.800');
const imageBorder = useColorValue('gray.600', 'whiteAlpha.800');
const fg = useOpposingColor(bg);
const checkedBorder = useColorValue('blue.400', 'blue.300');
const errorBorder = useColorValue('red.500', 'red.300');
const borderColor = useMemo(
() =>
hasError && isChecked
? // Highlight red when there are no overlapping query types for the locations selected.
errorBorder
: isChecked && !hasError
? // Highlight blue when any location is selected and there is no error.
checkedBorder
: // Otherwise, no border.
'transparent',
[hasError, isChecked, checkedBorder, errorBorder],
);
return (
<MotionBox
py={4}
px={6}
bg={bg}
w="100%"
minW="xs"
maxW="sm"
mx="auto"
shadow="sm"
key={label}
rounded="lg"
cursor="pointer"
borderWidth="1px"
borderStyle="solid"
whileHover={{ scale: 1.05 }}
borderColor={borderColor}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
handleChange(option);
}}
>
<Flex justifyContent="space-between" alignItems="center">
<chakra.h2
color={fg}
fontWeight="bold"
mt={{ base: 2, md: 0 }}
fontSize={{ base: 'lg', md: 'xl' }}
>
{label}
</chakra.h2>
<Avatar
color={fg}
fit="cover"
alt={label}
name={label}
boxSize={12}
rounded="full"
borderWidth={1}
bg="whiteAlpha.300"
borderStyle="solid"
borderColor={imageBorder}
src={(option.data?.avatar as string) ?? undefined}
/>
</Flex>
{option?.data?.description && (
<chakra.p mt={2} color={fg} opacity={0.6} fontSize="sm">
{option.data.description as string}
</chakra.p>
)}
</MotionBox>
);
};

View file

@ -1,18 +1,18 @@
import { useMemo, useState } from 'react'; import { useMemo } from 'react';
import { Wrap, VStack, Flex, Box, Avatar, chakra } from '@chakra-ui/react'; import { Wrap, VStack, Flex, chakra } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { Select } from '~/components'; import { Select } from '~/components';
import { useConfig, useColorValue } from '~/context'; import { useConfig } from '~/context';
import { useOpposingColor, useFormState } from '~/hooks'; import { useFormState } from '~/hooks';
import { isMultiValue, isSingleValue } from '~/components/select'; import { isMultiValue, isSingleValue } from '~/components/select';
import { LocationCard } from './location-card';
import type { DeviceGroup, SingleOption, OptionGroup, FormData } from '~/types'; import type { DeviceGroup, SingleOption, OptionGroup, FormData } from '~/types';
import type { SelectOnChange } from '~/components/select'; import type { SelectOnChange } from '~/components/select';
import type { TQuerySelectField, LocationCardProps } from './types'; import type { TQuerySelectField } from './types';
/** Location option type alias for future extensions. */ /** Location option type alias for future extensions. */
type LocationOption = SingleOption; export type LocationOption = SingleOption;
function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] { function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
return devices return devices
@ -37,101 +37,13 @@ function buildOptions(devices: DeviceGroup[]): OptionGroup<LocationOption>[] {
.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0)); .sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
} }
const MotionBox = motion(Box);
const LocationCard = (props: LocationCardProps): JSX.Element => {
const { option, onChange, defaultChecked, hasError } = props;
const { label } = option;
const [isChecked, setChecked] = useState(defaultChecked);
function handleChange(value: LocationOption) {
if (isChecked) {
setChecked(false);
onChange('remove', value);
} else {
setChecked(true);
onChange('add', value);
}
}
const bg = useColorValue('whiteAlpha.300', 'blackSolid.700');
const imageBorder = useColorValue('gray.600', 'whiteAlpha.800');
const fg = useOpposingColor(bg);
const checkedBorder = useColorValue('blue.400', 'blue.300');
const errorBorder = useColorValue('red.500', 'red.300');
const borderColor = useMemo(
() =>
hasError && isChecked
? // Highlight red when there are no overlapping query types for the locations selected.
errorBorder
: isChecked && !hasError
? // Highlight blue when any location is selected and there is no error.
checkedBorder
: // Otherwise, no border.
'transparent',
[hasError, isChecked, checkedBorder, errorBorder],
);
return (
<MotionBox
py={4}
px={6}
bg={bg}
w="100%"
minW="xs"
maxW="sm"
mx="auto"
shadow="lg"
key={label}
rounded="lg"
cursor="pointer"
borderWidth="1px"
borderStyle="solid"
whileHover={{ scale: 1.05 }}
borderColor={borderColor}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
handleChange(option);
}}
>
<Flex justifyContent="space-between" alignItems="center">
<chakra.h2
color={fg}
fontWeight="bold"
mt={{ base: 2, md: 0 }}
fontSize={{ base: 'lg', md: 'xl' }}
>
{label}
</chakra.h2>
<Avatar
color={fg}
fit="cover"
alt={label}
name={label}
boxSize={12}
rounded="full"
borderWidth={1}
bg="whiteAlpha.300"
borderStyle="solid"
borderColor={imageBorder}
src={(option.data?.avatar as string) ?? undefined}
/>
</Flex>
{option?.data?.description && (
<chakra.p mt={2} color={fg} opacity={0.6} fontSize="sm">
{option.data.description as string}
</chakra.p>
)}
</MotionBox>
);
};
export const QueryLocation = (props: TQuerySelectField): JSX.Element => { export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
const { onChange, label } = props; const { onChange, label } = props;
const { devices } = useConfig(); const {
devices,
web: { locationDisplayMode },
} = useConfig();
const { const {
formState: { errors }, formState: { errors },
} = useFormContext<FormData>(); } = useFormContext<FormData>();
@ -139,12 +51,18 @@ export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
const setSelection = useFormState(s => s.setSelection); const setSelection = useFormState(s => s.setSelection);
const { form, filtered } = useFormState(({ form, filtered }) => ({ form, filtered })); const { form, filtered } = useFormState(({ form, filtered }) => ({ form, filtered }));
const options = useMemo(() => buildOptions(devices), [devices]); const options = useMemo(() => buildOptions(devices), [devices]);
const element = useMemo(() => { const element = useMemo(() => {
if (locationDisplayMode === 'dropdown') {
return 'select';
} else if (locationDisplayMode === 'gallery') {
return 'cards';
}
const groups = options.length; const groups = options.length;
const maxOptionsPerGroup = Math.max(...options.map(opt => opt.options.length)); const maxOptionsPerGroup = Math.max(...options.map(opt => opt.options.length));
const showCards = groups < 5 && maxOptionsPerGroup < 6; const showCards = groups < 5 && maxOptionsPerGroup < 6;
return showCards ? 'cards' : 'select'; return showCards ? 'cards' : 'select';
}, [options]); }, [options, locationDisplayMode]);
const noOverlap = useMemo( const noOverlap = useMemo(
() => form.queryLocation.length > 1 && filtered.types.length === 0, () => form.queryLocation.length > 1 && filtered.types.length === 0,
@ -221,8 +139,8 @@ export const QueryLocation = (props: TQuerySelectField): JSX.Element => {
options={options} options={options}
aria-label={label} aria-label={label}
name="queryLocation" name="queryLocation"
onChange={handleSelectChange}
closeMenuOnSelect={false} closeMenuOnSelect={false}
onChange={handleSelectChange}
value={selections.queryLocation} value={selections.queryLocation}
isError={typeof errors.queryLocation !== 'undefined'} isError={typeof errors.queryLocation !== 'undefined'}
/> />

View file

@ -42,7 +42,7 @@ const Option = (props: OptionProps<OptionWithDescription, false>) => {
export const QueryTarget = (props: TQueryTarget): JSX.Element => { export const QueryTarget = (props: TQueryTarget): JSX.Element => {
const { name, register, onChange, placeholder } = props; const { name, register, onChange, placeholder } = props;
const bg = useColorValue('white', 'whiteAlpha.100'); const bg = useColorValue('white', 'blackSolid.800');
const color = useColorValue('gray.400', 'whiteAlpha.800'); const color = useColorValue('gray.400', 'whiteAlpha.800');
const border = useColorValue('gray.100', 'whiteAlpha.50'); const border = useColorValue('gray.100', 'whiteAlpha.50');
const placeholderColor = useColorValue('gray.600', 'whiteAlpha.700'); const placeholderColor = useColorValue('gray.600', 'whiteAlpha.700');

View file

@ -189,7 +189,7 @@ export const LookingGlass = (): JSX.Element => {
</FormField> </FormField>
</FormRow> </FormRow>
<FormRow> <FormRow>
<SlideFade offsetY={100} in={filtered.types.length > 0} unmountOnExit> <SlideFade offsetX={-100} in={filtered.types.length > 0} unmountOnExit>
<FormField <FormField
name="queryType" name="queryType"
label={web.text.queryType} label={web.text.queryType}

View file

@ -15,6 +15,7 @@ export const useControlStyle = <Opt extends SingleOption, IsMulti extends boolea
props: RSStyleCallbackProps, props: RSStyleCallbackProps,
): RSStyleFunction<'control', Opt, IsMulti> => { ): RSStyleFunction<'control', Opt, IsMulti> => {
const { colorMode } = props; const { colorMode } = props;
const { isError } = useSelectContext(); const { isError } = useSelectContext();
const minHeight = useToken('space', 12); const minHeight = useToken('space', 12);
@ -22,9 +23,10 @@ export const useControlStyle = <Opt extends SingleOption, IsMulti extends boolea
const color = useColorToken('colors', 'black', 'whiteAlpha.800'); const color = useColorToken('colors', 'black', 'whiteAlpha.800');
const focusBorder = useColorToken('colors', 'blue.500', 'blue.300'); const focusBorder = useColorToken('colors', 'blue.500', 'blue.300');
const invalidBorder = useColorToken('colors', 'red.500', 'red.300'); const invalidBorder = useColorToken('colors', 'red.500', 'red.300');
// const borderColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300');
const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50'); const borderColor = useColorToken('colors', 'gray.100', 'whiteAlpha.50');
const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400'); const borderHover = useColorToken('colors', 'gray.300', 'whiteAlpha.400');
const backgroundColor = useColorToken('colors', 'white', 'whiteAlpha.100'); const backgroundColor = useColorToken('colors', 'white', 'blackSolid.800');
return useCallback( return useCallback(
(base, state) => { (base, state) => {
@ -56,9 +58,12 @@ export const useMenuStyle = <Opt extends SingleOption, IsMulti extends boolean>(
props: RSStyleCallbackProps, props: RSStyleCallbackProps,
): RSStyleFunction<'menu', Opt, IsMulti> => { ): RSStyleFunction<'menu', Opt, IsMulti> => {
const { colorMode } = props; const { colorMode } = props;
const { isOpen } = useSelectContext(); const { isOpen } = useSelectContext();
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700'); const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
const styles = { backgroundColor, zIndex: 1500 }; const styles = { backgroundColor, zIndex: 1500 };
return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]); return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]);
}; };
@ -66,13 +71,14 @@ export const useMenuListStyle = <Opt extends SingleOption, IsMulti extends boole
props: RSStyleCallbackProps, props: RSStyleCallbackProps,
): RSStyleFunction<'menuList', Opt, IsMulti> => { ): RSStyleFunction<'menuList', Opt, IsMulti> => {
const { colorMode } = props; const { colorMode } = props;
const { isOpen } = useSelectContext(); const { isOpen } = useSelectContext();
const borderRadius = useToken('radii', 'md'); const borderRadius = useToken('radii', 'md');
const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700'); const backgroundColor = useColorToken('colors', 'white', 'blackSolid.700');
const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50'); const scrollbarTrack = useColorToken('colors', 'blackAlpha.50', 'whiteAlpha.50');
const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300'); const scrollbarThumb = useColorToken('colors', 'blackAlpha.300', 'whiteAlpha.300');
const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400'); const scrollbarThumbHover = useColorToken('colors', 'blackAlpha.400', 'whiteAlpha.400');
const styles = { const styles = {
borderRadius, borderRadius,
backgroundColor, backgroundColor,
@ -82,6 +88,7 @@ export const useMenuListStyle = <Opt extends SingleOption, IsMulti extends boole
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover }, '&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover },
'-ms-overflow-style': { display: 'none' }, '-ms-overflow-style': { display: 'none' },
}; };
return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]); return useCallback(base => mergeWith({}, base, styles), [colorMode, isOpen]);
}; };
@ -89,6 +96,7 @@ export const useOptionStyle = <Opt extends SingleOption, IsMulti extends boolean
props: RSStyleCallbackProps, props: RSStyleCallbackProps,
): RSStyleFunction<'option', Opt, IsMulti> => { ): RSStyleFunction<'option', Opt, IsMulti> => {
const { colorMode } = props; const { colorMode } = props;
const { isOpen } = useSelectContext(); const { isOpen } = useSelectContext();
const fontSize = useToken('fontSizes', 'lg'); const fontSize = useToken('fontSizes', 'lg');
@ -136,8 +144,10 @@ export const useIndicatorSeparatorStyle = <Opt extends SingleOption, IsMulti ext
props: RSStyleCallbackProps, props: RSStyleCallbackProps,
): RSStyleFunction<'indicatorSeparator', Opt, IsMulti> => { ): RSStyleFunction<'indicatorSeparator', Opt, IsMulti> => {
const { colorMode } = props; const { colorMode } = props;
const backgroundColor = useColorToken('colors', 'whiteAlpha.700', 'gray.600'); const backgroundColor = useColorToken('colors', 'gray.200', 'whiteAlpha.300');
// const backgroundColor = useColorToken('colors', 'gray.200', 'gray.600');
const styles = { backgroundColor }; const styles = { backgroundColor };
return useCallback(base => mergeWith({}, base, styles), [colorMode]); return useCallback(base => mergeWith({}, base, styles), [colorMode]);
}; };
@ -145,8 +155,10 @@ export const usePlaceholderStyle = <Opt extends SingleOption, IsMulti extends bo
props: RSStyleCallbackProps, props: RSStyleCallbackProps,
): RSStyleFunction<'placeholder', Opt, IsMulti> => { ): RSStyleFunction<'placeholder', Opt, IsMulti> => {
const { colorMode } = props; const { colorMode } = props;
const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700'); const color = useColorToken('colors', 'gray.600', 'whiteAlpha.700');
const fontSize = useToken('fontSizes', 'lg'); const fontSize = useToken('fontSizes', 'lg');
return useCallback(base => mergeWith({}, base, { color, fontSize }), [colorMode]); return useCallback(base => mergeWith({}, base, { color, fontSize }), [colorMode]);
}; };
@ -157,8 +169,8 @@ export const useSingleValueStyle = <Opt extends SingleOption, IsMulti extends bo
const color = useColorValue('black', 'whiteAlpha.800'); const color = useColorValue('black', 'whiteAlpha.800');
const fontSize = useToken('fontSizes', 'lg'); const fontSize = useToken('fontSizes', 'lg');
const styles = { color, fontSize }; const styles = { color, fontSize };
return useCallback(base => mergeWith({}, base, styles), [color, colorMode]); return useCallback(base => mergeWith({}, base, styles), [color, colorMode]);
}; };
@ -169,8 +181,9 @@ export const useMultiValueStyle = <Opt extends SingleOption, IsMulti extends boo
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor); const color = useOpposingColor(backgroundColor);
const borderRadius = useToken('radii', 'md');
const styles = { backgroundColor, color, borderRadius };
const styles = { backgroundColor, color };
return useCallback(base => mergeWith({}, base, styles), [backgroundColor, colorMode]); return useCallback(base => mergeWith({}, base, styles), [backgroundColor, colorMode]);
}; };
@ -181,8 +194,8 @@ export const useMultiValueLabelStyle = <Opt extends SingleOption, IsMulti extend
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor); const color = useOpposingColor(backgroundColor);
const styles = { color }; const styles = { color };
return useCallback(base => mergeWith({}, base, styles), [colorMode]); return useCallback(base => mergeWith({}, base, styles), [colorMode]);
}; };
@ -193,16 +206,17 @@ export const useMultiValueRemoveStyle = <Opt extends SingleOption, IsMulti exten
const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300'); const backgroundColor = useColorToken('colors', 'primary.500', 'primary.300');
const color = useOpposingColor(backgroundColor); const color = useOpposingColor(backgroundColor);
const styles = { const styles = {
color, color,
'&:hover': { backgroundColor: 'inherit', color, opacity: 0.7 }, '&:hover': { backgroundColor: 'transparent', color, opacity: 0.8 },
}; };
return useCallback(base => mergeWith({}, base, styles), [colorMode]); return useCallback(base => mergeWith({}, base, styles), [colorMode]);
}; };
export const useRSTheme = (): RSThemeFunction => { export const useRSTheme = (): RSThemeFunction => {
const borderRadius = useToken('radii', 'md'); const borderRadius = useToken('radii', 'md');
return useCallback((t: ReactSelect.Theme): ReactSelect.Theme => ({ ...t, borderRadius }), []); return useCallback((t: ReactSelect.Theme): ReactSelect.Theme => ({ ...t, borderRadius }), []);
}; };
@ -215,5 +229,6 @@ export const useMenuPortal = <Opt extends SingleOption, IsMulti extends boolean>
const styles = { const styles = {
zIndex: isMobile ? 1500 : 1, zIndex: isMobile ? 1500 : 1,
}; };
return useCallback(base => merge(base, styles), [isMobile]); return useCallback(base => merge(base, styles), [isMobile]);
}; };

View file

@ -98,6 +98,7 @@ interface _Web {
terms: { enable: boolean; title: string }; terms: { enable: boolean; title: string };
text: _Text; text: _Text;
theme: _ThemeConfig; theme: _ThemeConfig;
location_display_mode: 'auto' | 'gallery' | 'dropdown';
} }
type _DirectiveBase = { type _DirectiveBase = {