枫上雾棋的 storybook
  • CORE
  • COMPONENTS
  • SNIPPETS
utils
ds
hooks
style

search

import { parse, stringify } from "querystring";

type SearchQuery = Record<
  string,
  | string
  | number
  | boolean
  | ReadonlyArray<string>
  | ReadonlyArray<number>
  | ReadonlyArray<boolean>
  | null,
>;

export const toSearchString = (query: SearchQuery) => {
  const str = stringify(query);
  return str ? `?${str}` : "";
};

export const parseSearchString = (search: string) => {
  if (search.startsWith("?")) {
    return parse(search.slice(1));
  }

  return parse(search);
};
Source

pickElmAttrs

// pickElmAttrs.ts
import isPropValid from "@emotion/is-prop-valid";

export const pickElmAttrs = (props: Record<string, any>) => {
  const p: Record<string, any> = {};

  Object.keys(props).forEach(key => {
    if (isPropValid(key)) {
      p[key] = props[key];
    }
  });

  return p;
};

// app.tsx
const App = ({ ...otherprops }) => {
  return <div {...pickElmAttrs(otherProps)} />;};
Source

colorPalette

export const colorPalette = {
  black: "#000",
  white: "#fff",

  gray: {
    100: "#f7fafc",
    200: "#edf2f7",
    300: "#e2e8f0",
    400: "#cbd5e0",
    500: "#a0aec0",
    600: "#718096",
    700: "#4a5568",
    800: "#2d3748",
    900: "#1a202c",
  },
};
White
Black
Gray
Red
Orange
Yellow
Green
Teal
Blue
Indigo
Purple
Pink
Source

defaultTheme

// defaultTheme.ts
const color = {
  primary: colorPalette.blue[500],
  secondary: rgba(colorPalette.blue[500], 0.85),
};

export const defaultTheme = {  color,
  colorPalette,
};

// Layout.tsx
const storybookTheme = produce(defaultTheme, theme => {
  theme.color.primary = "#c2185b";
  theme.color.secondary = rgba("#c2185b", 0.85);
  theme.color.bg = "#fafafa";
});
PRIMARY
SECONDARY
SUCCESS
WARNING
ERROR
INFO
TEXT
TEXTLIGHT
BG
BGLIGHT
Source

reset

import { Global } from "@emotion/react";
import { normalize } from "polished";

export const DSReset = () => {
  const ds = useDesignSystem();

  return (
    <>
      <Global styles={{ ...normalize() }} />
      <Global
        styles={{
          a: {
            color: `${ds.color.primary}`,
            textDecoration: "none",
          },
        }}
      />
    </>
  );
};
Source

useDesignSystem

// useDesignSystem.ts
import { ThemeContext } from "@emotion/react";
import { ITheme } from "./defaultTheme";

export const useDesignSystem = () => {
  return useContext(ThemeContext as Context<ITheme>);};

// app.tsx
const App = () => {
  const ds = useDesignSystem();
  return <div css={{ background: ds.color.primary }} />;
};
Source

useHover

not hover
export const useHover = <T = any>(): [RefObject<T>, boolean] => {
  const [isHovered, setIsHovered] = useState(false);
  const hoverRef = useRef(null);

  useEffect(() => {
    const node = hoverRef.current;

    if (!node) {
      return;
    }

    const mouseover$ = fromEvent(node, "mouseover").subscribe(() =>
      setIsHovered(true),
    );
    const mouseout$ = fromEvent(node, "mouseout").subscribe(() =>
      setIsHovered(false),
    );

    return () => {
      mouseover$.unsubscribe();
      mouseout$.unsubscribe();
    };
  }, [hoverRef.current]);

  return [hoverRef, isHovered];
};
Source

useIsomorphicLayoutEffect

/*
 * use useIsomorphicLayoutEffect in place of useLayoutEffect to avoid SSR warning
 */
export const useIsomorphicLayoutEffect =
  typeof window !== "undefined" ? useLayoutEffect : useEffect;
Source

useObservable

0
type ObservableValueType<T> = T extends Observable<infer U> ? U : never;

export const useObservable = <T extends Observable<any>>(ob$: T) => {
  const [value, setValue] = useState<ObservableValueType<T>>(
    (ob$ as any).value,
  );

  useEffect(() => {
    const sub = ob$.subscribe(setValue);

    return () => {
      sub.unsubscribe();
    };
  }, [ob$]);

  return value;
};
Source

useClickOutside

export const useClickOutside = <T extends HTMLElement>(
  ref: RefObject<T>,
  handler: (event: Event) => void,
) => {
  useEffect(() => {
    const handleClick = (event: Event) => {
      if (ref.current && !ref.current.contains(event.target as Node)) {
        handler(event);
      }
    };

    const click$ = fromEvent(document, "click").subscribe(handleClick);

    return () => {
      click$.unsubscribe();
    };
  }, [ref, handler]);
};
Source

useToggle

on
const useToggle = (defaultVal = false) => {
  const { on$, show, hide, toggle } = useMemo(() => {
    const on$ = new BehaviorSubject(defaultVal);

    return {
      on$,
      show: () => {
        on$.next(true);
      },
      hide: () => {
        on$.next(false);
      },
      toggle: () => {
        on$.next(!on$.value);
      },
    };
  }, []);

  const on = useObservable(on$);

  return [on, { show, hide, toggle }] as const;
};
Source

useVisibilitySensor

1
2
3
4
5
export const useVisibilitySensor = (
  ref: RefObject<Element>,
  options: IntersectionObserverInit,
) => {
  const [isIntersecting, setIntersecting] = useState(false);

  useEffect(() => {
    const io = new IntersectionObserver(([entry]) => {
      setIntersecting(entry.isIntersecting);
    }, options);

    if (ref.current) {
      io.observe(ref.current);
    }

    return () => {
      io.disconnect();
    };
  }, []);

  return isIntersecting;
};
Source

flex

CSS FlexBox
export const flex = (flexOpts: CSSObject): CSSObject => ({
  display: "flex",
  ...flexOpts,
});

export const inlineFlex = (flexOpts: CSSObject): CSSObject => ({
  display: "inline-flex",
  ...flexOpts,
});

styled.div({
  ...flex({    justifyContent: "center",
    alignItems: "center",
  }),
});
Source

useRect

width: 0
export const getRect = (targetElm: Element, relatedElm: Element): IRect => {
  const targetRect = targetElm.getBoundingClientRect();
  const relatedRect = relatedElm.getBoundingClientRect();

  return {
    top: (targetRect.top || 0) - relatedRect.top,
    left: (targetRect.left || 0) - relatedRect.left,
    width: targetRect.width || 0,
    height: targetRect.height || 0,
  };
};

export const useRect = (
  elmRef: RefObject<Element | null>,
): [IRect, () => void] => {
  const [rect, setRect] = useState({ left: 0, top: 0, width: 0, height: 0 });

  const refreshRect = useCallback(() => {
    if (elmRef.current) {
      setRect(getRect(elmRef.current, document.body));
    }
  }, []);

  useEffect(() => {
    refreshRect();
  }, []);

  useEffect(() => {
    const resize$ = fromEvent(globalThis, "resize");
    const orientationchange$ = fromEvent(globalThis, "orientationchange");

    const sub = observableMerge(resize$, orientationchange$)
      .pipe(observeOn(animationFrameScheduler), debounceTime(200))
      .subscribe(refreshRect);

    return () => {
      sub.unsubscribe();
    };
  }, []);

  return [rect, refreshRect];
};
Source

fluidType

Fluid type is cool.

// $min-font-size + ($max-font-size - $min-font-size) * (100vw - $min-vw) / ($max-vw - $min-vw)
export const fluidType = (
  minVw: breakpointKey,
  maxVw: breakpointKey,
  minFontSize: number,
  maxFontSize: number,
): CSSObject => ({
  fontSize: minFontSize,
  [`@media (min-width: ${breakpoint[minVw]}px)`]: {
    fontSize: `calc(${minFontSize}px + (${
      maxFontSize - minFontSize
    }) * (100vw - ${breakpoint[minVw]}px) / (${
      breakpoint[maxVw] - breakpoint[minVw]
    }))`,
  },
  [`@media (min-width: ${breakpoint[maxVw]}px)`]: {
    fontSize: maxFontSize,
  },
});

styled.div({
  ...fluidType("md", "xl", 12, 16),});
Source

grid

1
2
3
4
5
export const grid = (gridOpts: CSSObject): CSSObject => ({
  display: "grid",
  ...gridOpts,
});

styled.div({
  ...grid({    gridTemplateColumns: "repeat(3, 1fr)",
    gridTemplateRows: "repeat(3, 1fr)",
    gridAutoFlow: "row dense",
    gridGap: `${ds.spacing[5]}`,
  }),
});
Source

mq

Media Queries.

import facepaint from "facepaint";

enum breakpoint {
  sm = 640,
  md = 768,
  lg = 1024,
  xl = 1280,
}
type breakpointKey = keyof typeof breakpoint;

export const mq = (breakpoints: breakpointKey[], style: CSSObject) => {
  const selectors = breakpoints.map(
    bp => `@media (min-width: ${breakpoint[bp]}px)`,
  );
  const mq = facepaint(selectors);

  const dynamicStyle = mq(style);

  return dynamicStyle;
};

styled.div(mq(["lg"], {  color: [ds.color.secondary, ds.color.text],
}))
Source