// 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)} />;};
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);
};
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",
},
};
// 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";
});
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",
},
}}
/>
</>
);
};
// 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 }} />;
};
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]);
};
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];
};
/*
* use useIsomorphicLayoutEffect in place of useLayoutEffect to avoid SSR warning
*/
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
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;
};
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];
};
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;
};
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;
};
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",
}),
});
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),});
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]}`,
}),
});
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],
}))