mirror of
https://github.com/thatmattlove/hyperglass.git
synced 2026-01-17 08:48:05 +00:00
improve header layout [skip ci]
This commit is contained in:
parent
9886020c53
commit
ac36f42d82
20 changed files with 527 additions and 327 deletions
|
|
@ -3,3 +3,6 @@ import { motion } from 'framer-motion';
|
|||
|
||||
export const AnimatedDiv = motion.custom(chakra.div);
|
||||
export const AnimatedForm = motion.custom(chakra.form);
|
||||
export const AnimatedH1 = motion.custom(chakra.h1);
|
||||
export const AnimatedH3 = motion.custom(chakra.h3);
|
||||
export const AnimatedButton = motion.custom(chakra.button);
|
||||
|
|
|
|||
14
hyperglass/ui/components/header/context.ts
Normal file
14
hyperglass/ui/components/header/context.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { createContext, useContext } from 'react';
|
||||
import { createState, useState } from '@hookstate/core';
|
||||
import type { THeaderCtx, THeaderState } from './types';
|
||||
|
||||
const HeaderCtx = createContext<THeaderCtx>({
|
||||
showSubtitle: true,
|
||||
titleRef: {} as React.MutableRefObject<HTMLHeadingElement>,
|
||||
});
|
||||
|
||||
export const HeaderProvider = HeaderCtx.Provider;
|
||||
export const useHeaderCtx = (): THeaderCtx => useContext(HeaderCtx);
|
||||
|
||||
const HeaderState = createState<THeaderState>({ fontSize: '' });
|
||||
export const useHeader = () => useState<THeaderState>(HeaderState);
|
||||
|
|
@ -1,168 +1,60 @@
|
|||
import { Flex } from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { AnimatedDiv, Title, ResetButton, ColorModeToggle } from '~/components';
|
||||
import { useColorValue, useConfig, useBreakpointValue } from '~/context';
|
||||
import { useRef } from 'react';
|
||||
import { Flex, ScaleFade } from '@chakra-ui/react';
|
||||
import { ColorModeToggle } from '~/components';
|
||||
import { useColorValue, useBreakpointValue } from '~/context';
|
||||
import { useBooleanValue, useLGState } from '~/hooks';
|
||||
import { HeaderProvider } from './context';
|
||||
import { Title } from './title';
|
||||
|
||||
import type { ResponsiveValue } from '@chakra-ui/react';
|
||||
import type { THeader, TTitleMode, THeaderLayout } from './types';
|
||||
|
||||
const headerTransition = {
|
||||
type: 'spring',
|
||||
ease: 'anticipate',
|
||||
damping: 15,
|
||||
stiffness: 100,
|
||||
};
|
||||
|
||||
function getWidth(mode: TTitleMode): ResponsiveValue<string> {
|
||||
let width = '100%' as ResponsiveValue<string>;
|
||||
|
||||
switch (mode) {
|
||||
case 'text_only':
|
||||
width = '100%';
|
||||
break;
|
||||
case 'logo_only':
|
||||
width = { base: '90%', lg: '50%' };
|
||||
break;
|
||||
case 'logo_subtitle':
|
||||
width = { base: '90%', lg: '50%' };
|
||||
break;
|
||||
case 'all':
|
||||
width = { base: '90%', lg: '50%' };
|
||||
break;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
import type { THeader } from './types';
|
||||
|
||||
export const Header = (props: THeader) => {
|
||||
const { resetForm, ...rest } = props;
|
||||
|
||||
const bg = useColorValue('white', 'black');
|
||||
|
||||
const { web } = useConfig();
|
||||
const { isSubmitting } = useLGState();
|
||||
|
||||
const mlResetButton = useBooleanValue(isSubmitting.value, { base: 0, md: 2 }, undefined);
|
||||
const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' });
|
||||
const titleRef = useRef({} as HTMLDivElement);
|
||||
|
||||
const titleVariant = useBreakpointValue({
|
||||
base: {
|
||||
fullSize: { scale: 1, marginLeft: 0 },
|
||||
smallLogo: { marginLeft: 'auto' },
|
||||
smallText: { marginLeft: 'auto' },
|
||||
},
|
||||
md: {
|
||||
fullSize: { scale: 1 },
|
||||
smallLogo: { scale: 0.5 },
|
||||
smallText: { scale: 0.8 },
|
||||
},
|
||||
lg: {
|
||||
fullSize: { scale: 1 },
|
||||
smallLogo: { scale: 0.5 },
|
||||
smallText: { scale: 0.8 },
|
||||
},
|
||||
xl: {
|
||||
fullSize: { scale: 1 },
|
||||
smallLogo: { scale: 0.5 },
|
||||
smallText: { scale: 0.8 },
|
||||
},
|
||||
});
|
||||
|
||||
const titleJustify = useBooleanValue(
|
||||
const titleWidth = useBooleanValue(
|
||||
isSubmitting.value,
|
||||
{ base: 'flex-end', md: 'center' },
|
||||
{ base: 'flex-start', md: 'center' },
|
||||
);
|
||||
const resetButton = (
|
||||
<AnimatePresence key="resetButton">
|
||||
<AnimatedDiv
|
||||
transition={headerTransition}
|
||||
initial={{ opacity: 0, x: -50 }}
|
||||
animate={{ opacity: 1, x: 0, width: 'unset' }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
alignItems="center"
|
||||
mb={{ md: 'auto' }}
|
||||
ml={mlResetButton}
|
||||
display={isSubmitting ? 'flex' : 'none'}>
|
||||
<motion.div>
|
||||
<ResetButton onClick={resetForm} />
|
||||
</motion.div>
|
||||
</AnimatedDiv>
|
||||
</AnimatePresence>
|
||||
);
|
||||
const title = (
|
||||
<AnimatedDiv
|
||||
key="title"
|
||||
px={1}
|
||||
alignItems={isSubmitting ? 'center' : ['center', 'center', 'flex-end', 'flex-end']}
|
||||
transition={headerTransition}
|
||||
initial={{ scale: 0.5 }}
|
||||
animate={
|
||||
isSubmitting && web.text.title_mode === 'text_only'
|
||||
? 'smallText'
|
||||
: isSubmitting && web.text.title_mode !== 'text_only'
|
||||
? 'smallLogo'
|
||||
: 'fullSize'
|
||||
}
|
||||
variants={titleVariant}
|
||||
justifyContent={titleJustify}
|
||||
mt={[null, isSubmitting ? null : 'auto']}
|
||||
maxW={getWidth(web.text.title_mode)}
|
||||
flex="1 0 0"
|
||||
minH={titleHeight}>
|
||||
<Title onClick={resetForm} />
|
||||
</AnimatedDiv>
|
||||
);
|
||||
const colorModeToggle = (
|
||||
<AnimatedDiv
|
||||
transition={headerTransition}
|
||||
key="colorModeToggle"
|
||||
alignItems="center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
mb={[null, 'auto']}
|
||||
mr={isSubmitting ? undefined : 2}>
|
||||
<ColorModeToggle />
|
||||
</AnimatedDiv>
|
||||
{ base: '75%', lg: '50%' },
|
||||
{ base: '75%', lg: '75%' },
|
||||
);
|
||||
|
||||
const layout = useBooleanValue(
|
||||
isSubmitting.value,
|
||||
{
|
||||
sm: [resetButton, colorModeToggle, title],
|
||||
md: [resetButton, title, colorModeToggle],
|
||||
lg: [resetButton, title, colorModeToggle],
|
||||
xl: [resetButton, title, colorModeToggle],
|
||||
},
|
||||
{
|
||||
sm: [title, resetButton, colorModeToggle],
|
||||
md: [resetButton, title, colorModeToggle],
|
||||
lg: [resetButton, title, colorModeToggle],
|
||||
xl: [resetButton, title, colorModeToggle],
|
||||
},
|
||||
) as THeaderLayout;
|
||||
|
||||
const layoutBp: keyof THeaderLayout =
|
||||
useBreakpointValue({ base: 'sm', md: 'md', lg: 'lg', xl: 'xl' }) ?? 'sm';
|
||||
const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
|
||||
|
||||
return (
|
||||
<Flex
|
||||
px={2}
|
||||
zIndex="4"
|
||||
as="header"
|
||||
width="full"
|
||||
flex="0 1 auto"
|
||||
bg={bg}
|
||||
color="gray.500"
|
||||
{...rest}>
|
||||
<HeaderProvider value={{ showSubtitle: !isSubmitting.value, titleRef }}>
|
||||
<Flex
|
||||
w="100%"
|
||||
mx="auto"
|
||||
px={4}
|
||||
pt={6}
|
||||
justify="space-between"
|
||||
flex="1 0 auto"
|
||||
alignItems={isSubmitting ? 'center' : 'flex-start'}>
|
||||
{layout[layoutBp]}
|
||||
bg={bg}
|
||||
minH={16}
|
||||
zIndex={4}
|
||||
as="header"
|
||||
width="full"
|
||||
flex="0 1 auto"
|
||||
color="gray.500"
|
||||
{...rest}>
|
||||
<ScaleFade in initialScale={0.5} style={{ width: '100%' }}>
|
||||
<Flex
|
||||
d="flex"
|
||||
key="title"
|
||||
height="100%"
|
||||
ref={titleRef}
|
||||
mx={{ base: 0, lg: 'auto' }}
|
||||
maxW={titleWidth}
|
||||
// This is here for the logo
|
||||
justifyContent={justify}>
|
||||
<Title />
|
||||
</Flex>
|
||||
</ScaleFade>
|
||||
{/* <Flex pos="absolute" right={0} top={0} m={4}>
|
||||
<ColorModeToggle />
|
||||
</Flex> */}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</HeaderProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
73
hyperglass/ui/components/header/logo.tsx
Normal file
73
hyperglass/ui/components/header/logo.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { Image, Skeleton } from '@chakra-ui/react';
|
||||
import { useColorValue, useConfig, useColorMode } from '~/context';
|
||||
|
||||
import type { TLogo } from './types';
|
||||
|
||||
/**
|
||||
* Custom hook to handle loading the user's logo, errors loading the logo, and color mode changes.
|
||||
*/
|
||||
function useLogo(): [string, () => void] {
|
||||
const { web } = useConfig();
|
||||
const { dark_format, light_format } = web.logo;
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`);
|
||||
|
||||
// Use the hyperglass logo if the user's logo can't be loaded for whatever reason.
|
||||
const fallbackSrc = useColorValue(
|
||||
'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-dark.svg',
|
||||
'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg',
|
||||
);
|
||||
|
||||
const [fallback, setSource] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* If the user image cannot be loaded, log an error to the console and set the fallback image.
|
||||
*/
|
||||
function setFallback() {
|
||||
console.warn(`Error loading image from '${src}'`);
|
||||
setSource(fallbackSrc);
|
||||
}
|
||||
|
||||
// Only return the fallback image if it's been set.
|
||||
return useMemo(() => [fallback ?? src, setFallback], [colorMode]);
|
||||
}
|
||||
|
||||
export const Logo = (props: TLogo) => {
|
||||
const { web } = useConfig();
|
||||
const { width } = web.logo;
|
||||
|
||||
const skeletonA = useColorValue('whiteFaded.100', 'blackFaded.800');
|
||||
const skeletonB = useColorValue('original.light', 'original.dark');
|
||||
|
||||
const [source, setFallback] = useLogo();
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={source}
|
||||
alt={web.text.title}
|
||||
onError={setFallback}
|
||||
width={width ?? 'auto'}
|
||||
css={{
|
||||
userDrag: 'none',
|
||||
userSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
WebkitUserDrag: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
}}
|
||||
fallback={
|
||||
<Skeleton
|
||||
isLoaded={false}
|
||||
borderRadius="md"
|
||||
endColor={skeletonB}
|
||||
startColor={skeletonA}
|
||||
width={{ base: 64, lg: 80 }}
|
||||
height={{ base: 12, lg: 16 }}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
20
hyperglass/ui/components/header/subtitleOnly.tsx
Normal file
20
hyperglass/ui/components/header/subtitleOnly.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Heading } from '@chakra-ui/react';
|
||||
import { useConfig, useBreakpointValue } from '~/context';
|
||||
import { useTitleSize } from './useTitleSize';
|
||||
|
||||
export const SubtitleOnly = () => {
|
||||
const { web } = useConfig();
|
||||
const sizeSm = useTitleSize(web.text.subtitle, 'sm');
|
||||
const fontSize = useBreakpointValue({ base: sizeSm, lg: 'xl' });
|
||||
|
||||
return (
|
||||
<Heading
|
||||
as="h3"
|
||||
fontWeight="normal"
|
||||
fontSize={fontSize}
|
||||
whiteSpace="break-spaces"
|
||||
textAlign={{ base: 'left', xl: 'center' }}>
|
||||
{web.text.subtitle}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
143
hyperglass/ui/components/header/title.tsx
Normal file
143
hyperglass/ui/components/header/title.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { Flex, Button, VStack } from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { If } from '~/components';
|
||||
import { useConfig, useMobile } from '~/context';
|
||||
import { useBooleanValue, useLGState } from '~/hooks';
|
||||
import { Logo } from './logo';
|
||||
import { TitleOnly } from './titleOnly';
|
||||
import { useHeaderCtx } from './context';
|
||||
import { SubtitleOnly } from './subtitleOnly';
|
||||
|
||||
import type { StackProps } from '@chakra-ui/react';
|
||||
import type { TTitle, TTitleWrapper, TDWrapper } from './types';
|
||||
|
||||
const AnimatedVStack = motion.custom(VStack);
|
||||
|
||||
/**
|
||||
* Title wrapper for mobile devices, breakpoints sm & md.
|
||||
*/
|
||||
const MWrapper = (props: StackProps) => <VStack spacing={1} alignItems="flex-start" {...props} />;
|
||||
|
||||
/**
|
||||
* Title wrapper for desktop devices, breakpoints lg & xl.
|
||||
*/
|
||||
const DWrapper = (props: TDWrapper) => {
|
||||
const { showSubtitle } = useHeaderCtx();
|
||||
return (
|
||||
<AnimatedVStack
|
||||
spacing={1}
|
||||
initial="main"
|
||||
alignItems="center"
|
||||
animate={showSubtitle ? 'main' : 'submitting'}
|
||||
transition={{ damping: 15, type: 'spring', stiffness: 100 }}
|
||||
variants={{ submitting: { scale: 0.5 }, main: { scale: 1 } }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Universal wrapper for title sub-components, which will be different depending on the
|
||||
* `title_mode` configuration variable.
|
||||
*/
|
||||
const TitleWrapper = (props: TDWrapper | StackProps) => {
|
||||
const isMobile = useMobile();
|
||||
return (
|
||||
<>
|
||||
{isMobile ? <MWrapper {...(props as StackProps)} /> : <DWrapper {...(props as TDWrapper)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Title sub-component if `title_mode` is set to `text_only`.
|
||||
*/
|
||||
const TextOnly = (props: TTitleWrapper) => {
|
||||
return (
|
||||
<TitleWrapper {...props}>
|
||||
<TitleOnly />
|
||||
<SubtitleOnly />
|
||||
</TitleWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Title sub-component if `title_mode` is set to `logo_only`. Renders only the logo.
|
||||
*/
|
||||
const LogoOnly = () => (
|
||||
<TitleWrapper>
|
||||
<Logo />
|
||||
</TitleWrapper>
|
||||
);
|
||||
|
||||
/**
|
||||
* Title sub-component if `title_mode` is set to `logo_subtitle`. Renders the logo with the
|
||||
* subtitle underneath.
|
||||
*/
|
||||
const LogoSubtitle = () => (
|
||||
<TitleWrapper>
|
||||
<Logo />
|
||||
<SubtitleOnly />
|
||||
</TitleWrapper>
|
||||
);
|
||||
|
||||
/**
|
||||
* Title sub-component if `title_mode` is set to `all`. Renders the logo, title, and subtitle.
|
||||
*/
|
||||
const All = () => (
|
||||
<TitleWrapper>
|
||||
<Logo />
|
||||
<TextOnly mt={2} />
|
||||
</TitleWrapper>
|
||||
);
|
||||
|
||||
/**
|
||||
* Title component which renders sub-components based on the `title_mode` configuration variable.
|
||||
*/
|
||||
export const Title = (props: TTitle) => {
|
||||
const { fontSize, ...rest } = props;
|
||||
const { web } = useConfig();
|
||||
const titleMode = web.text.title_mode;
|
||||
|
||||
const { isSubmitting, resetForm } = useLGState();
|
||||
|
||||
const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' });
|
||||
|
||||
function handleClick(): void {
|
||||
isSubmitting.set(false);
|
||||
resetForm();
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
px={0}
|
||||
flexWrap="wrap"
|
||||
flexDir="column"
|
||||
minH={titleHeight}
|
||||
justifyContent="center"
|
||||
mt={[null, isSubmitting.value ? null : 'auto']}
|
||||
{...rest}>
|
||||
<Button
|
||||
px={0}
|
||||
variant="link"
|
||||
flexWrap="wrap"
|
||||
flexDir="column"
|
||||
onClick={handleClick}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
_hover={{ textDecoration: 'none' }}>
|
||||
<If c={titleMode === 'text_only'}>
|
||||
<TextOnly />
|
||||
</If>
|
||||
<If c={titleMode === 'logo_only'}>
|
||||
<LogoOnly />
|
||||
</If>
|
||||
<If c={titleMode === 'logo_subtitle'}>
|
||||
<LogoSubtitle />
|
||||
</If>
|
||||
<If c={titleMode === 'all'}>
|
||||
<All />
|
||||
</If>
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
19
hyperglass/ui/components/header/titleOnly.tsx
Normal file
19
hyperglass/ui/components/header/titleOnly.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Heading } from '@chakra-ui/react';
|
||||
import { useConfig } from '~/context';
|
||||
import { useBooleanValue } from '~/hooks';
|
||||
import { useHeaderCtx } from './context';
|
||||
import { useTitleSize } from './useTitleSize';
|
||||
|
||||
export const TitleOnly = () => {
|
||||
const { showSubtitle } = useHeaderCtx();
|
||||
const { web } = useConfig();
|
||||
|
||||
const margin = useBooleanValue(showSubtitle, 2, 0);
|
||||
const sizeSm = useTitleSize(web.text.title, '2xl', []);
|
||||
|
||||
return (
|
||||
<Heading as="h1" mb={margin} fontSize={{ base: sizeSm, lg: '5xl' }}>
|
||||
{web.text.title}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,16 +1,31 @@
|
|||
import { FlexProps } from '@chakra-ui/react';
|
||||
|
||||
import { IConfig } from '~/types';
|
||||
import type { FlexProps, HeadingProps, ImageProps, StackProps } from '@chakra-ui/react';
|
||||
import type { MotionProps } from 'framer-motion';
|
||||
|
||||
export interface THeader extends FlexProps {
|
||||
resetForm(): void;
|
||||
}
|
||||
|
||||
export type TTitleMode = IConfig['web']['text']['title_mode'];
|
||||
|
||||
export type THeaderLayout = {
|
||||
sm: [JSX.Element, JSX.Element, JSX.Element];
|
||||
md: [JSX.Element, JSX.Element, JSX.Element];
|
||||
lg: [JSX.Element, JSX.Element, JSX.Element];
|
||||
xl: [JSX.Element, JSX.Element, JSX.Element];
|
||||
sm: [JSX.Element, JSX.Element];
|
||||
md: [JSX.Element, JSX.Element];
|
||||
lg: [JSX.Element, JSX.Element];
|
||||
xl: [JSX.Element, JSX.Element];
|
||||
};
|
||||
export type TDWrapper = Omit<StackProps, 'transition'> & MotionProps;
|
||||
|
||||
export interface TTitle extends FlexProps {}
|
||||
|
||||
export interface TTitleOnly extends HeadingProps {}
|
||||
|
||||
export interface TLogo extends ImageProps {}
|
||||
|
||||
export interface TTitleWrapper extends Partial<MotionProps & Omit<StackProps, 'transition'>> {}
|
||||
|
||||
export interface THeaderCtx {
|
||||
showSubtitle: boolean;
|
||||
titleRef: React.MutableRefObject<HTMLHeadingElement>;
|
||||
}
|
||||
|
||||
export interface THeaderState {
|
||||
fontSize: string;
|
||||
}
|
||||
|
|
|
|||
59
hyperglass/ui/components/header/useTitleSize.ts
Normal file
59
hyperglass/ui/components/header/useTitleSize.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useToken } from '@chakra-ui/react';
|
||||
import { useMobile } from '~/context';
|
||||
|
||||
// Mobile:
|
||||
// xs: 32
|
||||
// sm: 28
|
||||
// md: 24
|
||||
// lg: 20
|
||||
// xl: 16
|
||||
// 2xl: 14
|
||||
// 3xl: 12
|
||||
// 4xl: 10
|
||||
// 5xl: 7
|
||||
type Sizes = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||
|
||||
export function useTitleSize(title: string, defaultSize: Sizes, deps: any[] = []) {
|
||||
const [size, setSize] = useState<Sizes>(defaultSize);
|
||||
const realSize = useToken('fontSizes', size);
|
||||
const isMobile = useMobile();
|
||||
function getSize(l: number): void {
|
||||
switch (true) {
|
||||
case l > 32:
|
||||
setSize('xs');
|
||||
break;
|
||||
case l <= 32 && l > 28:
|
||||
setSize('xs');
|
||||
break;
|
||||
case l <= 28 && l > 24:
|
||||
setSize('sm');
|
||||
break;
|
||||
case l <= 24 && l > 20:
|
||||
setSize('md');
|
||||
break;
|
||||
case l <= 20 && l > 16:
|
||||
setSize('lg');
|
||||
break;
|
||||
case l <= 16 && l > 14:
|
||||
setSize('xl');
|
||||
break;
|
||||
case l <= 14 && l > 12:
|
||||
setSize('2xl');
|
||||
break;
|
||||
case l <= 12 && l > 10:
|
||||
setSize('3xl');
|
||||
break;
|
||||
case l <= 10 && l > 7:
|
||||
setSize('4xl');
|
||||
break;
|
||||
case l <= 7:
|
||||
setSize('5xl');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return useMemo(() => {
|
||||
getSize(title.length);
|
||||
return realSize;
|
||||
}, [title, isMobile, ...deps]);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from './title';
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { Image } from '@chakra-ui/react';
|
||||
import { useColorValue, useConfig } from '~/context';
|
||||
|
||||
import type { TLogo } from './types';
|
||||
|
||||
export const Logo = (props: TLogo) => {
|
||||
const { web } = useConfig();
|
||||
const { width, dark_format, light_format } = web.logo;
|
||||
|
||||
const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`);
|
||||
const fallbackSrc = useColorValue(
|
||||
'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-dark.svg',
|
||||
'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg',
|
||||
);
|
||||
|
||||
return (
|
||||
<Image
|
||||
css={{
|
||||
userDrag: 'none',
|
||||
userSelect: 'none',
|
||||
msUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
WebkitUserDrag: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
}}
|
||||
fallbackSrc={fallbackSrc}
|
||||
width={width ?? 'auto'}
|
||||
alt={web.text.title}
|
||||
src={src}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { Heading } from '@chakra-ui/react';
|
||||
import { useConfig } from '~/context';
|
||||
|
||||
import type { TSubtitleOnly } from './types';
|
||||
|
||||
export const SubtitleOnly = (props: TSubtitleOnly) => {
|
||||
const { web } = useConfig();
|
||||
return (
|
||||
<Heading
|
||||
as="h3"
|
||||
whiteSpace="break-spaces"
|
||||
fontSize={{ base: 'md', lg: 'xl' }}
|
||||
textAlign={{ base: 'left', xl: 'center' }}
|
||||
{...props}>
|
||||
{web.text.subtitle}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { forwardRef } from 'react';
|
||||
import { Button, Stack } from '@chakra-ui/react';
|
||||
import { If } from '~/components';
|
||||
import { useConfig } from '~/context';
|
||||
import { useBooleanValue, useLGState } from '~/hooks';
|
||||
import { TitleOnly } from './titleOnly';
|
||||
import { SubtitleOnly } from './subtitleOnly';
|
||||
import { Logo } from './logo';
|
||||
|
||||
import type { TTitle, TTextOnly } from './types';
|
||||
|
||||
const TextOnly = (props: TTextOnly) => {
|
||||
const { showSubtitle, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={2}
|
||||
maxW="100%"
|
||||
textAlign={showSubtitle ? ['right', 'center'] : ['left', 'center']}
|
||||
{...rest}>
|
||||
<TitleOnly showSubtitle={showSubtitle} />
|
||||
<If c={showSubtitle}>
|
||||
<SubtitleOnly />
|
||||
</If>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const LogoSubtitle = () => (
|
||||
<>
|
||||
<Logo />
|
||||
<SubtitleOnly mt={6} />
|
||||
</>
|
||||
);
|
||||
|
||||
const All = (props: TTextOnly) => {
|
||||
const { showSubtitle, ...rest } = props;
|
||||
return (
|
||||
<>
|
||||
<Logo />
|
||||
<TextOnly showSubtitle={showSubtitle} mt={2} {...rest} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Title = forwardRef<HTMLButtonElement, TTitle>((props, ref) => {
|
||||
const { web } = useConfig();
|
||||
const titleMode = web.text.title_mode;
|
||||
|
||||
const { isSubmitting } = useLGState();
|
||||
|
||||
const justify = useBooleanValue(
|
||||
isSubmitting.value,
|
||||
{ base: 'flex-end', md: 'center' },
|
||||
{ base: 'flex-start', md: 'center' },
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
px={0}
|
||||
w="100%"
|
||||
ref={ref}
|
||||
variant="link"
|
||||
flexWrap="wrap"
|
||||
flexDir="column"
|
||||
justifyContent={justify}
|
||||
_focus={{ boxShadow: 'none' }}
|
||||
_hover={{ textDecoration: 'none' }}
|
||||
alignItems={{ base: 'flex-start', lg: 'center' }}
|
||||
{...props}>
|
||||
<If c={titleMode === 'text_only'}>
|
||||
<TextOnly showSubtitle={!isSubmitting.value} />
|
||||
</If>
|
||||
<If c={titleMode === 'logo_only'}>
|
||||
<Logo />
|
||||
</If>
|
||||
<If c={titleMode === 'logo_subtitle'}>
|
||||
<LogoSubtitle />
|
||||
</If>
|
||||
<If c={titleMode === 'all'}>
|
||||
<All showSubtitle={!isSubmitting.value} />
|
||||
</If>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { Heading } from '@chakra-ui/react';
|
||||
import { useConfig } from '~/context';
|
||||
import { useBooleanValue } from '~/hooks';
|
||||
|
||||
import type { TTitleOnly } from './types';
|
||||
|
||||
export const TitleOnly = (props: TTitleOnly) => {
|
||||
const { showSubtitle, ...rest } = props;
|
||||
const { web } = useConfig();
|
||||
const fontSize = useBooleanValue(showSubtitle, { base: '2xl', lg: '5xl' }, '2xl');
|
||||
const margin = useBooleanValue(showSubtitle, 2, 0);
|
||||
return (
|
||||
<Heading as="h1" mb={margin} fontSize={fontSize} {...rest}>
|
||||
{web.text.title}
|
||||
</Heading>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import type { ButtonProps, HeadingProps, ImageProps, StackProps } from '@chakra-ui/react';
|
||||
|
||||
export interface TTitle extends ButtonProps {}
|
||||
|
||||
export interface TTitleOnly extends HeadingProps {
|
||||
showSubtitle: boolean;
|
||||
}
|
||||
|
||||
export interface TSubtitleOnly extends HeadingProps {}
|
||||
|
||||
export interface TLogo extends ImageProps {}
|
||||
|
||||
export interface TTextOnly extends StackProps {
|
||||
showSubtitle: boolean;
|
||||
}
|
||||
|
|
@ -8,3 +8,5 @@ export * from './useOpposingColor';
|
|||
export * from './useSessionStorage';
|
||||
export * from './useStrf';
|
||||
export * from './useTableToString';
|
||||
export * from './useScaledText';
|
||||
export * from './useScaledTitle';
|
||||
|
|
|
|||
33
hyperglass/ui/hooks/useScaledText.ts
Normal file
33
hyperglass/ui/hooks/useScaledText.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import type { UseMeasureRef as UM } from 'react-use/esm/useMeasure';
|
||||
|
||||
/**
|
||||
* These type aliases are for the readability of the function below.
|
||||
*/
|
||||
type DC = HTMLElement;
|
||||
type DT = HTMLHeadingElement;
|
||||
type A = any;
|
||||
type B = boolean;
|
||||
|
||||
/**
|
||||
* Wrapper for useMeasure() which determines if a text element should be scaled down due to its
|
||||
* size relative to its parent's size.
|
||||
*/
|
||||
export function useScaledText<C extends DC = DC, T extends DT = DT>(deps: A[]): [UM<C>, UM<T>, B] {
|
||||
// Get a ref & state object for the containing element.
|
||||
const [containerRef, container] = useMeasure<C>();
|
||||
|
||||
// Get a ref & state object for the text element.
|
||||
const [textRef, text] = useMeasure<T>();
|
||||
|
||||
// Memoize the values.
|
||||
const textWidth = useMemo(() => text.width, [...deps, text.width !== 0]);
|
||||
const containerWidth = useMemo(() => container.width, [...deps, container.width]);
|
||||
|
||||
// If the text element is the same size or larger than the container, it should be resized.
|
||||
const shouldResize = textWidth !== 0 && textWidth >= containerWidth;
|
||||
|
||||
return [containerRef, textRef, shouldResize];
|
||||
}
|
||||
62
hyperglass/ui/hooks/useScaledTitle.ts
Normal file
62
hyperglass/ui/hooks/useScaledTitle.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type ScaledTitleCallback = (f: string) => void;
|
||||
|
||||
function getWidthPx<R extends React.MutableRefObject<HTMLElement>>(ref: R) {
|
||||
const computedStyle = window.getComputedStyle(ref.current);
|
||||
const widthStr = computedStyle.width.replaceAll('px', '');
|
||||
const width = parseFloat(widthStr);
|
||||
return width;
|
||||
}
|
||||
|
||||
function reducePx(px: number) {
|
||||
return px * 0.9;
|
||||
}
|
||||
|
||||
function reducer(val: number, tooBig: () => boolean): number {
|
||||
let r = val;
|
||||
if (tooBig()) {
|
||||
r = reducePx(val);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* useScaledTitle(
|
||||
* f => {
|
||||
* setFontsize(f);
|
||||
* },
|
||||
* titleRef,
|
||||
* ref,
|
||||
* [showSubtitle],
|
||||
* );
|
||||
*/
|
||||
export function useScaledTitle<
|
||||
P extends React.MutableRefObject<HTMLDivElement>,
|
||||
T extends React.MutableRefObject<HTMLHeadingElement>
|
||||
>(callback: ScaledTitleCallback, parentRef: P, titleRef: T, deps: any[] = []) {
|
||||
console.log(deps);
|
||||
const [fontSize, setFontSize] = useState('');
|
||||
const calcSize = useRef(0);
|
||||
|
||||
function effect() {
|
||||
const computedSize = window.getComputedStyle(titleRef.current).getPropertyValue('font-size');
|
||||
|
||||
const fontPx = parseFloat(computedSize.replaceAll('px', ''));
|
||||
calcSize.current = fontPx;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
calcSize.current = reducer(
|
||||
calcSize.current,
|
||||
() => getWidthPx(titleRef) >= getWidthPx(parentRef),
|
||||
);
|
||||
|
||||
setFontSize(`${calcSize.current}px`);
|
||||
|
||||
return callback(fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
return useEffect(effect, [...deps, callback]);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { TValidQueryTypes, TStringTableData, TQueryResponseString } from './data';
|
||||
import type { TValidQueryTypes, TStringTableData, TQueryResponseString } from './data';
|
||||
import type { TQueryContent } from './config';
|
||||
|
||||
export function isQueryType(q: any): q is TValidQueryTypes {
|
||||
let result = false;
|
||||
|
|
@ -22,3 +23,7 @@ export function isStructuredOutput(data: any): data is TStringTableData {
|
|||
export function isStringOutput(data: any): data is TQueryResponseString {
|
||||
return typeof data !== 'undefined' && 'output' in data && typeof data.output === 'string';
|
||||
}
|
||||
|
||||
export function isQueryContent(c: any): c is TQueryContent {
|
||||
return typeof c !== 'undefined' && c !== null && 'content' in c;
|
||||
}
|
||||
|
|
|
|||
29
hyperglass/ui/types/react-textfit.d.ts
vendored
Normal file
29
hyperglass/ui/types/react-textfit.d.ts
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
declare module 'react-textfit' {
|
||||
type RenderFunction = (text: string) => React.ReactNode;
|
||||
interface TextfitProps {
|
||||
children: React.ReactNode | RenderFunction;
|
||||
text?: string;
|
||||
/**
|
||||
* @default number 1
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
* @default number 100
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* @default single|multi multi
|
||||
*/
|
||||
mode?: 'single' | 'multi';
|
||||
/**
|
||||
* @default boolean true
|
||||
*/
|
||||
forceSingleModeWidth?: boolean;
|
||||
/**
|
||||
* @default number 50
|
||||
*/
|
||||
throttle?: number;
|
||||
onReady?: (mid: number) => void;
|
||||
}
|
||||
class Textfit extends React.Component<TextfitProps> {}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue