1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

improve header layout [skip ci]

This commit is contained in:
checktheroads
2020-12-29 02:05:28 -07:00
parent 9886020c53
commit ac36f42d82
20 changed files with 527 additions and 327 deletions

View File

@@ -3,3 +3,6 @@ import { motion } from 'framer-motion';
export const AnimatedDiv = motion.custom(chakra.div); export const AnimatedDiv = motion.custom(chakra.div);
export const AnimatedForm = motion.custom(chakra.form); 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);

View 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);

View File

@@ -1,168 +1,60 @@
import { Flex } from '@chakra-ui/react'; import { useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { Flex, ScaleFade } from '@chakra-ui/react';
import { AnimatedDiv, Title, ResetButton, ColorModeToggle } from '~/components'; import { ColorModeToggle } from '~/components';
import { useColorValue, useConfig, useBreakpointValue } from '~/context'; import { useColorValue, useBreakpointValue } from '~/context';
import { useBooleanValue, useLGState } from '~/hooks'; import { useBooleanValue, useLGState } from '~/hooks';
import { HeaderProvider } from './context';
import { Title } from './title';
import type { ResponsiveValue } from '@chakra-ui/react'; import type { THeader } from './types';
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;
}
export const Header = (props: THeader) => { export const Header = (props: THeader) => {
const { resetForm, ...rest } = props; const { resetForm, ...rest } = props;
const bg = useColorValue('white', 'black'); const bg = useColorValue('white', 'black');
const { web } = useConfig();
const { isSubmitting } = useLGState(); const { isSubmitting } = useLGState();
const mlResetButton = useBooleanValue(isSubmitting.value, { base: 0, md: 2 }, undefined); const titleRef = useRef({} as HTMLDivElement);
const titleHeight = useBooleanValue(isSubmitting.value, undefined, { md: '20vh' });
const titleVariant = useBreakpointValue({ const titleWidth = useBooleanValue(
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(
isSubmitting.value, isSubmitting.value,
{ base: 'flex-end', md: 'center' }, { base: '75%', lg: '50%' },
{ base: 'flex-start', md: 'center' }, { base: '75%', lg: '75%' },
);
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>
); );
const layout = useBooleanValue( const justify = useBreakpointValue({ base: 'flex-start', lg: 'center' });
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';
return ( return (
<Flex <HeaderProvider value={{ showSubtitle: !isSubmitting.value, titleRef }}>
px={2}
zIndex="4"
as="header"
width="full"
flex="0 1 auto"
bg={bg}
color="gray.500"
{...rest}>
<Flex <Flex
w="100%" px={4}
mx="auto"
pt={6} pt={6}
justify="space-between" bg={bg}
flex="1 0 auto" minH={16}
alignItems={isSubmitting ? 'center' : 'flex-start'}> zIndex={4}
{layout[layoutBp]} 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>
</Flex> </HeaderProvider>
); );
}; };

View 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}
/>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -1,16 +1,31 @@
import { FlexProps } from '@chakra-ui/react'; import type { FlexProps, HeadingProps, ImageProps, StackProps } from '@chakra-ui/react';
import type { MotionProps } from 'framer-motion';
import { IConfig } from '~/types';
export interface THeader extends FlexProps { export interface THeader extends FlexProps {
resetForm(): void; resetForm(): void;
} }
export type TTitleMode = IConfig['web']['text']['title_mode'];
export type THeaderLayout = { export type THeaderLayout = {
sm: [JSX.Element, JSX.Element, JSX.Element]; sm: [JSX.Element, JSX.Element];
md: [JSX.Element, JSX.Element, JSX.Element]; md: [JSX.Element, JSX.Element];
lg: [JSX.Element, JSX.Element, JSX.Element]; lg: [JSX.Element, JSX.Element];
xl: [JSX.Element, 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;
}

View 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]);
}

View File

@@ -1 +0,0 @@
export * from './title';

View File

@@ -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}
/>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
});

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -8,3 +8,5 @@ export * from './useOpposingColor';
export * from './useSessionStorage'; export * from './useSessionStorage';
export * from './useStrf'; export * from './useStrf';
export * from './useTableToString'; export * from './useTableToString';
export * from './useScaledText';
export * from './useScaledTitle';

View 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];
}

View 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]);
}

View File

@@ -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 { export function isQueryType(q: any): q is TValidQueryTypes {
let result = false; let result = false;
@@ -22,3 +23,7 @@ export function isStructuredOutput(data: any): data is TStringTableData {
export function isStringOutput(data: any): data is TQueryResponseString { export function isStringOutput(data: any): data is TQueryResponseString {
return typeof data !== 'undefined' && 'output' in data && typeof data.output === 'string'; 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
View 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> {}
}