forked from mirrors/thatmattlove-hyperglass
cleanup & animation improvement
This commit is contained in:
parent
ba2ce4b930
commit
4ee97ec0b2
11 changed files with 57 additions and 74 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { chakra, Box, forwardRef } from '@chakra-ui/react';
|
import { chakra } from '@chakra-ui/react';
|
||||||
import { motion, isValidMotionProp } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import type { BoxProps } from '@chakra-ui/react';
|
import type { BoxProps } from '@chakra-ui/react';
|
||||||
import type { CustomDomComponent, Transition, MotionProps } from 'framer-motion';
|
import type { CustomDomComponent, Transition, MotionProps } from 'framer-motion';
|
||||||
|
|
@ -10,19 +10,6 @@ type MakeMotionProps<P extends BoxProps> = React.PropsWithChildren<
|
||||||
Omit<P, 'transition'> & Omit<MotionProps, 'transition'> & { transition?: Transition }
|
Omit<P, 'transition'> & Omit<MotionProps, 'transition'> & { transition?: Transition }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Combined Chakra + Framer Motion component.
|
|
||||||
* @see https://chakra-ui.com/guides/integrations/with-framer
|
|
||||||
*/
|
|
||||||
export const AnimatedDiv = motion(
|
|
||||||
forwardRef<BoxProps, React.ElementType<BoxProps>>((props, ref) => {
|
|
||||||
const chakraProps = Object.fromEntries(
|
|
||||||
Object.entries(props).filter(([key]) => !isValidMotionProp(key)),
|
|
||||||
);
|
|
||||||
return <Box ref={ref} {...chakraProps} />;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine `chakra` and `motion` factories.
|
* Combine `chakra` and `motion` factories.
|
||||||
*
|
*
|
||||||
|
|
@ -37,3 +24,5 @@ export function motionChakra<P extends BoxProps = BoxProps>(
|
||||||
// @ts-expect-error I don't know how to fix this.
|
// @ts-expect-error I don't know how to fix this.
|
||||||
return motion<P>(chakra<MCComponent, P>(component, options));
|
return motion<P>(chakra<MCComponent, P>(component, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AnimatedDiv = motionChakra('div');
|
||||||
|
|
@ -1,21 +1,18 @@
|
||||||
import { useRef } from 'react';
|
|
||||||
import { Flex, ScaleFade } from '@chakra-ui/react';
|
import { Flex, ScaleFade } from '@chakra-ui/react';
|
||||||
import { AnimatedDiv } from '~/components';
|
import { motionChakra } from '~/components';
|
||||||
import { useBreakpointValue } from '~/context';
|
import { useBreakpointValue } from '~/context';
|
||||||
import { useBooleanValue, useFormState } from '~/hooks';
|
import { useBooleanValue, useFormInteractive } from '~/hooks';
|
||||||
import { Title } from './title';
|
import { Title } from './title';
|
||||||
|
|
||||||
import type { THeader } from './types';
|
const Wrapper = motionChakra('header', {
|
||||||
|
baseStyle: { display: 'flex', px: 4, pt: 6, minH: 16, w: 'full', flex: '0 1 auto' },
|
||||||
|
});
|
||||||
|
|
||||||
export const Header = (props: THeader): JSX.Element => {
|
export const Header = (): JSX.Element => {
|
||||||
const { resetForm, ...rest } = props;
|
const formInteractive = useFormInteractive();
|
||||||
|
|
||||||
const status = useFormState(s => s.status);
|
|
||||||
|
|
||||||
const titleRef = useRef({} as HTMLDivElement);
|
|
||||||
|
|
||||||
const titleWidth = useBooleanValue(
|
const titleWidth = useBooleanValue(
|
||||||
status === 'results',
|
formInteractive,
|
||||||
{ base: '75%', lg: '50%' },
|
{ base: '75%', lg: '50%' },
|
||||||
{ base: '75%', lg: '75%' },
|
{ base: '75%', lg: '75%' },
|
||||||
);
|
);
|
||||||
|
|
@ -23,21 +20,18 @@ export const Header = (props: THeader): JSX.Element => {
|
||||||
const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
|
const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex px={4} pt={6} minH={16} zIndex={4} as="header" width="full" flex="0 1 auto" {...rest}>
|
<Wrapper layout="position">
|
||||||
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
|
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
|
||||||
<AnimatedDiv
|
<Flex
|
||||||
layout
|
|
||||||
height="100%"
|
height="100%"
|
||||||
display="flex"
|
|
||||||
ref={titleRef}
|
|
||||||
maxW={titleWidth}
|
maxW={titleWidth}
|
||||||
// This is here for the logo
|
// This is here for the logo
|
||||||
justifyContent={justify}
|
justifyContent={justify}
|
||||||
mx={{ base: status === 'results' ? 'auto' : 0, lg: 'auto' }}
|
mx={{ base: formInteractive ? 'auto' : 0, lg: 'auto' }}
|
||||||
>
|
>
|
||||||
<Title />
|
<Title />
|
||||||
</AnimatedDiv>
|
</Flex>
|
||||||
</ScaleFade>
|
</ScaleFade>
|
||||||
</Flex>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { motion } from 'framer-motion';
|
||||||
import { isSafari } from 'react-device-detect';
|
import { isSafari } from 'react-device-detect';
|
||||||
import { Switch, Case } from 'react-if';
|
import { Switch, Case } from 'react-if';
|
||||||
import { useConfig, useMobile } from '~/context';
|
import { useConfig, useMobile } from '~/context';
|
||||||
import { useBooleanValue, useFormState } from '~/hooks';
|
import { useFormState, useFormInteractive } from '~/hooks';
|
||||||
import { SubtitleOnly } from './subtitleOnly';
|
import { SubtitleOnly } from './subtitleOnly';
|
||||||
import { TitleOnly } from './titleOnly';
|
import { TitleOnly } from './titleOnly';
|
||||||
import { Logo } from './logo';
|
import { Logo } from './logo';
|
||||||
|
|
@ -11,17 +11,18 @@ import { Logo } from './logo';
|
||||||
import type { TTitle, TTitleWrapper, TDWrapper, TMWrapper } from './types';
|
import type { TTitle, TTitleWrapper, TDWrapper, TMWrapper } from './types';
|
||||||
|
|
||||||
const AnimatedVStack = motion(VStack);
|
const AnimatedVStack = motion(VStack);
|
||||||
|
const AnimatedFlex = motion(Flex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Title wrapper for mobile devices, breakpoints sm & md.
|
* Title wrapper for mobile devices, breakpoints sm & md.
|
||||||
*/
|
*/
|
||||||
const MWrapper = (props: TMWrapper): JSX.Element => {
|
const MWrapper = (props: TMWrapper): JSX.Element => {
|
||||||
const status = useFormState(s => s.status);
|
const formInteractive = useFormInteractive();
|
||||||
return (
|
return (
|
||||||
<AnimatedVStack
|
<AnimatedVStack
|
||||||
layout
|
layout
|
||||||
spacing={1}
|
spacing={1}
|
||||||
alignItems={status === 'results' ? 'center' : 'flex-start'}
|
alignItems={formInteractive ? 'center' : 'flex-start'}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -31,13 +32,13 @@ const MWrapper = (props: TMWrapper): JSX.Element => {
|
||||||
* Title wrapper for desktop devices, breakpoints lg & xl.
|
* Title wrapper for desktop devices, breakpoints lg & xl.
|
||||||
*/
|
*/
|
||||||
const DWrapper = (props: TDWrapper): JSX.Element => {
|
const DWrapper = (props: TDWrapper): JSX.Element => {
|
||||||
const status = useFormState(s => s.status);
|
const formInteractive = useFormInteractive();
|
||||||
return (
|
return (
|
||||||
<AnimatedVStack
|
<AnimatedVStack
|
||||||
spacing={1}
|
spacing={1}
|
||||||
initial="main"
|
initial="main"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
animate={status}
|
animate={formInteractive}
|
||||||
transition={{ damping: 15, type: 'spring', stiffness: 100 }}
|
transition={{ damping: 15, type: 'spring', stiffness: 100 }}
|
||||||
variants={{ results: { scale: 0.5 }, form: { scale: 1 } }}
|
variants={{ results: { scale: 0.5 }, form: { scale: 1 } }}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -108,19 +109,15 @@ export const Title = (props: TTitle): JSX.Element => {
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const { titleMode } = web.text;
|
const { titleMode } = web.text;
|
||||||
|
|
||||||
const { status, reset } = useFormState(({ status, reset }) => ({
|
const reset = useFormState(s => s.reset);
|
||||||
status,
|
const formInteractive = useFormInteractive();
|
||||||
reset,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const titleHeight = useBooleanValue(status === 'results', undefined, { md: '20vh' });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<AnimatedFlex
|
||||||
px={0}
|
px={0}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
flexDir="column"
|
flexDir="column"
|
||||||
minH={titleHeight}
|
animate={{ height: formInteractive ? undefined : '20vh' }}
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
/* flexBasis
|
/* flexBasis
|
||||||
This is a fix for Safari specifically. LMGTFY: Safari flex-basis width. Nutshell: Safari
|
This is a fix for Safari specifically. LMGTFY: Safari flex-basis width. Nutshell: Safari
|
||||||
|
|
@ -129,7 +126,7 @@ export const Title = (props: TTitle): JSX.Element => {
|
||||||
div up to the parent's max-width. The fix is to hard-code a flex-basis width.
|
div up to the parent's max-width. The fix is to hard-code a flex-basis width.
|
||||||
*/
|
*/
|
||||||
flexBasis={{ base: '100%', lg: isSafari ? '33%' : '100%' }}
|
flexBasis={{ base: '100%', lg: isSafari ? '33%' : '100%' }}
|
||||||
mt={{ md: status === 'results' ? undefined : 'auto' }}
|
mt={{ md: formInteractive ? undefined : 'auto' }}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -156,6 +153,6 @@ export const Title = (props: TTitle): JSX.Element => {
|
||||||
</Case>
|
</Case>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</AnimatedFlex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Heading } from '@chakra-ui/react';
|
import { Heading } from '@chakra-ui/react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { useBooleanValue, useFormState } from '~/hooks';
|
import { useBooleanValue, useFormInteractive } from '~/hooks';
|
||||||
import { useTitleSize } from './useTitleSize';
|
import { useTitleSize } from './useTitleSize';
|
||||||
|
|
||||||
export const TitleOnly = (): JSX.Element => {
|
export const TitleOnly = (): JSX.Element => {
|
||||||
const { web } = useConfig();
|
const { web } = useConfig();
|
||||||
const status = useFormState(s => s.status);
|
const formInteractive = useFormInteractive();
|
||||||
const margin = useBooleanValue(status === 'results', 0, 2);
|
const margin = useBooleanValue(formInteractive, 0, 2);
|
||||||
const sizeSm = useTitleSize(web.text.title, '2xl', []);
|
const sizeSm = useTitleSize(web.text.title, '2xl', []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
export * from './animated';
|
||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './codeBlock';
|
export * from './codeBlock';
|
||||||
export * from './countdown';
|
export * from './countdown';
|
||||||
export * from './custom';
|
export * from './custom';
|
||||||
export * from './debugger';
|
export * from './debugger';
|
||||||
|
export * from './dynamic-icon';
|
||||||
export * from './favicon';
|
export * from './favicon';
|
||||||
export * from './footer';
|
export * from './footer';
|
||||||
export * from './form';
|
export * from './form';
|
||||||
|
|
@ -23,4 +25,3 @@ export * from './results';
|
||||||
export * from './select';
|
export * from './select';
|
||||||
export * from './submit';
|
export * from './submit';
|
||||||
export * from './table';
|
export * from './table';
|
||||||
export * from './util';
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { Flex } from '@chakra-ui/react';
|
import { Flex } from '@chakra-ui/react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { isSafari } from 'react-device-detect';
|
import { isSafari } from 'react-device-detect';
|
||||||
import { If, Then } from 'react-if';
|
import { If, Then } from 'react-if';
|
||||||
import { Debugger, Greeting, Footer, Header } from '~/components';
|
import { Debugger, Greeting, Footer, Header } from '~/components';
|
||||||
|
|
@ -9,6 +10,8 @@ import { ResetButton } from './resetButton';
|
||||||
|
|
||||||
import type { TFrame } from './types';
|
import type { TFrame } from './types';
|
||||||
|
|
||||||
|
const AnimatedFlex = motion(Flex);
|
||||||
|
|
||||||
export const Frame = (props: TFrame): JSX.Element => {
|
export const Frame = (props: TFrame): JSX.Element => {
|
||||||
const { developerMode } = useConfig();
|
const { developerMode } = useConfig();
|
||||||
const { setStatus, reset } = useFormState(
|
const { setStatus, reset } = useFormState(
|
||||||
|
|
@ -38,19 +41,25 @@ export const Frame = (props: TFrame): JSX.Element => {
|
||||||
*/
|
*/
|
||||||
minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
|
minHeight={isSafari ? '-webkit-fill-available' : '100vh'}
|
||||||
>
|
>
|
||||||
<Header resetForm={() => handleReset()} />
|
<Header />
|
||||||
<Flex
|
<AnimatedFlex
|
||||||
|
layout
|
||||||
px={4}
|
px={4}
|
||||||
py={0}
|
py={0}
|
||||||
w="100%"
|
w="100%"
|
||||||
as="main"
|
as="main"
|
||||||
align="center"
|
|
||||||
flex="1 1 auto"
|
flex="1 1 auto"
|
||||||
justify="start"
|
|
||||||
flexDir="column"
|
flexDir="column"
|
||||||
textAlign="center"
|
textAlign="center"
|
||||||
{...props}
|
alignItems="center"
|
||||||
/>
|
justifyContent="start"
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
exit={{ opacity: 0, x: -300 }}
|
||||||
|
initial={{ opacity: 0, y: 300 }}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</AnimatedFlex>
|
||||||
<Footer />
|
<Footer />
|
||||||
<If condition={developerMode}>
|
<If condition={developerMode}>
|
||||||
<Then>
|
<Then>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import isEqual from 'react-fast-compare';
|
import isEqual from 'react-fast-compare';
|
||||||
import { Flex, ScaleFade, SlideFade } from '@chakra-ui/react';
|
import { chakra, Flex, ScaleFade, SlideFade } from '@chakra-ui/react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { vestResolver } from '@hookform/resolvers/vest';
|
import { vestResolver } from '@hookform/resolvers/vest';
|
||||||
import vest, { test, enforce } from 'vest';
|
import vest, { test, enforce } from 'vest';
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
FormField,
|
FormField,
|
||||||
HelpModal,
|
HelpModal,
|
||||||
QueryType,
|
QueryType,
|
||||||
AnimatedDiv,
|
|
||||||
QueryTarget,
|
QueryTarget,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
QueryLocation,
|
QueryLocation,
|
||||||
|
|
@ -169,17 +168,12 @@ export const LookingGlass = (): JSX.Element => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...formInstance}>
|
<FormProvider {...formInstance}>
|
||||||
<AnimatedDiv
|
<chakra.form
|
||||||
p={0}
|
p={0}
|
||||||
my={4}
|
my={4}
|
||||||
w="100%"
|
w="100%"
|
||||||
as="form"
|
|
||||||
mx="auto"
|
mx="auto"
|
||||||
textAlign="left"
|
textAlign="left"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
exit={{ opacity: 0, x: -300 }}
|
|
||||||
initial={{ opacity: 0, y: 300 }}
|
|
||||||
maxW={{ base: '100%', lg: '75%' }}
|
maxW={{ base: '100%', lg: '75%' }}
|
||||||
onSubmit={handleSubmit(submitHandler)}
|
onSubmit={handleSubmit(submitHandler)}
|
||||||
>
|
>
|
||||||
|
|
@ -232,7 +226,7 @@ export const LookingGlass = (): JSX.Element => {
|
||||||
</ScaleFade>
|
</ScaleFade>
|
||||||
</Flex>
|
</Flex>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
</AnimatedDiv>
|
</chakra.form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './animated';
|
|
||||||
export * from './dynamic-icon';
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export interface TIf {
|
|
||||||
c: boolean;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
@ -235,3 +235,8 @@ export function useView(): FormStatus {
|
||||||
return ready ? 'results' : 'form';
|
return ready ? 'results' : 'form';
|
||||||
}, [status, form]);
|
}, [status, form]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFormInteractive(): boolean {
|
||||||
|
const { status, selections } = useFormState(({ status, selections }) => ({ status, selections }));
|
||||||
|
return status === 'results' || selections.queryLocation.length > 0;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue