Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The dream that turned into nightmare (lightning)

The dream that turned into nightmare (lightning)

Radoslav Stankov

July 20, 2024
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. "

  2. "Dream" is short for "the dream to not write custom

    CSS". Let's hope v2 is not named "Nightmare" # Goals - Have consistent UI on the website - Make it easier to build new pages - Implement Product Hunt v6 design - Fully type safe What - Foundational utilities - Core UI components
  3. "Dream" is short for "the dream to not write custom

    CSS". Let's hope v2 is not named "Nightmare" # Goals - Have consistent UI on the website - Make it easier to build new pages - Implement Product Hunt v6 design - Fully type safe What - Foundational utilities - Core UI components $
  4. Core Design System - 4px grid - font size -

    font weights - color tokens - layouts
  5. import { Text } from '~/components/dream' <Text as="h1" /> <Text.Title>Title</Text.Title>

    <Text.Subtile>Subtile<Text./Subtile> <Text.Bold>Bold</Text.Bold>
  6. import { Button } from 'ph/components/dream' <Button.Solid label="text" icon={TwitterIcon} />

    <Button.Outline label="text" /> <Button.Text label="text" /> <Button.Icon icon={TwitterIcon} />
  7. import { Button } from 'ph/components/dream' <Button.Solid to={paths.post(post)} /> <Button.Solid

    onClick={fn} /> <Button.Solid mutation={GRAPHQL_MUTATION} /> <Button.Solid confirm="Are you sure" /> <Button.Solid requireLogin={true} />
  8. import { Form } from 'ph/components/dream' <Form.Mutation mutation={GRAPHQL_MUTATION} onSubmit={onSubmit}> <Form.Label

    label="First name" flex={1}> <Form.Input name="first_name" placeholder="Placeholder" /> </Form.Label> <Form.Label label="Last name" flex={1}> <Form.Input name="last_name" placeholder="Placeholder" /> </Form.Label> <Form.Submit /> <Form.Status /> </Form.Mutation>
  9. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  10. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  11. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  12. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  13. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  14. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  15. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile
  16. <Text>{content}</Text> <Text fontSize={16}>{content}</Text> <Text fontSize={16} p={1}>{content}</Text> <Text fontSize={16} p={{ mobile:

    1, tablet: 2, desktop: 3}}>{content}</Text> <Text fontSize={{ mobile: 12, tablet: 14, desktop: 16}} p={{ mobile: 1, tablet: 2, <div className="fontSize-14">{content}</div> <div className="fontSize-16">{content}</div> <div className="fontSize-16 p-1">{content}</div> <div className="fontSize-16 p-mobile-1 p-table-2 p-desktop-3">{content}</div> <div className="fontSize-mobile-12 fontSize-mobile-14 fontSize-deskopt-16 p-mobile % &
  17. interface IProps extends ITextCSSProps { children: React.ReactNode; className?: string; as?:

    'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'strong' | 'p' | 'div' | 'i' | 'legend' | 'details' | 'footer' | 'summary'; id?: string; onClick?: React.MouseEventHandler; format?: boolean; noOfLines?: INoOfLines; center?: boolean; target?: '_blank' | '_self'; 'data-test'?: string; 'aria-label'?: string; ellipsis?: boolean; } export default function Text({ children, className, as, id, color = 'darker-grey', fontSize = 16, fontWeight = 400, format = false, center = false, noOfLines, 'data-test': dataTest, onClick, ellipsis, ...otherProps }: IProps) { const finalClassName = classNames( dreamCSSClasses({ ...otherProps, color, fontSize, fontWeight, noOfLines }), className, format && styles.format, center && styles.center, ellipsis && styles.ellipsis, ); const Component = as ?? 'div'; return ( <Component className={finalClassName} id={id} data-test={dataTest} onClick={onClick}> {children} </Component> ); }
  18. export function deprecatedDreamCSSClasses(props: IDreamCSS) { let classes: string = '';

    const propNames = Object.keys(props); for (const propName of propNames) { const propValue = props[propName]; if (!ALLOWED_PROPS[propName]) { continue; } if (typeof propValue === 'object') { classes = classesForObject(classes, propName, propValue); } else { classes = classesForProps(classes, propName, propValue); } } return classes.trim(); } function classesForObject(classes: string, prop: string, props: any) { if (isNotSet(props.widescreen) && props.desktop) { props.widescreen = props.desktop; }
  19. function classesForObject(classes: string, prop: string, props: any) { if (isNotSet(props.widescreen)

    && props.desktop) { props.widescreen = props.desktop; } if (isNotSet(props.tablet) && props.mobile) { props.tablet = props.mobile; } const modifiers = Object.keys(props); for (const modifier of modifiers) { const propValue = props[modifier]; if (modifier === 'l') { classes = classesForProps(classes, prop, propValue); } else { classes = classesForProps(classes, `${prop}-${modifier}`, propValue); } } return classes; } function classesForProps(classes: string, propName: string, propValue: any) { if (typeof propValue === 'boolean' && propValue) { classes += `${propName}-default `; } else {
  20. const modifiers = Object.keys(props); for (const modifier of modifiers) {

    const propValue = props[modifier]; if (modifier === 'l') { classes = classesForProps(classes, prop, propValue); } else { classes = classesForProps(classes, `${prop}-${modifier}`, propValue); } } return classes; } function classesForProps(classes: string, propName: string, propValue: any) { if (typeof propValue === 'boolean' && propValue) { classes += `${propName}-default `; } else { classes += `${propName}-${propValue} `; } return classes; } function isNotSet(value) { return typeof value === 'undefined' || value === null; }
  21. ' Problem #1 - CSS bundle size All possible class

    names must be pre-generated and included in the CSS bundle because they are generated dynamically.
  22. ' Problem #1 - CSS bundle size gap: 14 sizes

    * 5 variants = 46 margin: 7 props * 14 sizes * 5 variants = 350 padding: 7 props * 14 sizes * 5 variants = 350 colors: 2 props * 12 colors * 5 variants = 120 ... ... total: ~2000 class names (
  23. ) Problem #2 - too many loops dreamCSSClasses was called

    for many components to build their class names. This results in loops and variable generation, resulting in many GCs and slowing rendering. Especially the initial rendering.
  24. ) Problem #2 - too many loops dreamCSSClasses was called

    for many components to build their class names. This results in loops and variable generation, resulting in many GCs and slowing rendering. Especially the initial rendering.
  25. ) Problem #2 - too many loops 2000+ calls to

    dreamCSSClasses for single render
  26. * Problem #3 - too many props Core components like

    Text and Flex, accepted subsets of dreamCSSClasses props, leading to overcomplicated code.
  27. interface IProps extends ITextCSSProps { children: React.ReactNode; className?: string; as?:

    'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'span' | 'strong' | 'p' | 'div' | 'i' | 'legend' | 'details' | 'footer' | 'summary'; id?: string; onClick?: React.MouseEventHandler; format?: boolean; noOfLines?: INoOfLines; center?: boolean; target?: '_blank' | '_self'; 'data-test'?: string; 'aria-label'?: string; ellipsis?: boolean; } export default function Text({ children, className, as, id, color = 'darker-grey', fontSize = 16, fontWeight = 400, format = false, center = false, noOfLines, 'data-test': dataTest, onClick, ellipsis, ...otherProps }: IProps) { const finalClassName = classNames( dreamCSSClasses({ ...otherProps, color, fontSize, fontWeight, noOfLines }), className, format && styles.format, center && styles.center, ellipsis && styles.ellipsis, ); const Component = as ?? 'div'; return ( <Component className={finalClassName} id={id} data-test={dataTest} onClick={onClick}> {children} </Component> ); }