Skip to content

Commit

Permalink
feat: skeleton labs
Browse files Browse the repository at this point in the history
  • Loading branch information
junghyeonsu committed Sep 11, 2024
1 parent 17d24be commit 330484e
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 22 deletions.
2 changes: 1 addition & 1 deletion component-docs/seed-design/ui/skeleton.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const fadeIn = keyframes({
});

export const root = style({
animation: `${fadeIn} 0.2s ease-in-out`,
animation: `${fadeIn} var(--skeleton-init-transition-duration, 0.2s) ease-in-out`,
});

export const skeleton = recipe({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import Layout from "@/src/activities/ActivityLayout";
import { Skeleton } from "@/seed-design/ui/skeleton";
import {
useSkeletonDuration,
useIsRealLoading,
useSkeletonLoading,
useSkeletonTimingFunction,
useSkeletonInitTransitionDuration,
} from "@/src/stores/skeleton";

declare module "@stackflow/config" {
Expand All @@ -30,21 +32,24 @@ const Fallback = () => {

const SkeletonWaveActivity: ActivityComponentType<"SkeletonWave"> = () => {
const isLoading = useSkeletonLoading();
const isRealLoading = useIsRealLoading();
const animationDuration = useSkeletonDuration();
const animationTiming = useSkeletonTimingFunction();
const initTransitionDuration = useSkeletonInitTransitionDuration();

return (
<Layout>
<div
style={
{
padding: "16px",
"--skeleton-init-transition-duration": initTransitionDuration,
"--skeleton-animation-duration": animationDuration,
"--skeleton-animation-timing-function": animationTiming,
} as React.CSSProperties
}
>
{isLoading ? <Fallback /> : <div>content</div>}
{isLoading ? isRealLoading && <Fallback /> : <div>content</div>}
</div>
</Layout>
);
Expand Down
22 changes: 21 additions & 1 deletion component-docs/src/components/SkeletonControls.css.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { style } from "@vanilla-extract/css";
import { keyframes, style } from "@vanilla-extract/css";

export const controlTitle = style({
fontSize: "16px",
fontWeight: "bold",
marginTop: "16px",
marginBottom: "8px",
});

export const controlBlock = style({
display: "flex",
Expand All @@ -11,3 +18,16 @@ export const controlInput = style({
flex: 1,
border: "1px solid #e5e5e5",
});

export const adapt = style({
marginTop: "20px",
});

export const leftToRight = keyframes({
from: {
left: "0",
},
to: {
left: "100%",
},
});
221 changes: 206 additions & 15 deletions component-docs/src/components/SkeletonControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,60 +5,251 @@ import {
useSkeletonGradient,
useSkeletonLoading,
useSkeletonTimingFunction,
useLoadingDurationMs,
useSkeletonInitTransitionDuration,
useRealLoadingStartTimeMs,
useIsRealLoading,
} from "@/src/stores/skeleton";

import * as React from "react";
import { controlBlock, controlInput, controlLabel } from "./SkeletonControls.css";
import * as css from "./SkeletonControls.css";

export const SkeletonControls = () => {
const { setControls } = useSkeletonActions();

const isLoading = useSkeletonLoading();
const { toggleLoading, setControls } = useSkeletonActions();
const isRealLoading = useIsRealLoading();
const [realLoadingStartTime, setRealLoadingStartTime] = React.useState(
useRealLoadingStartTimeMs(),
);
const loadingDurationInStore = useLoadingDurationMs();
const [duration, setDuration] = React.useState(useSkeletonDuration());
const [gradient, setGradient] = React.useState(useSkeletonGradient());
const [timingFunction, setTimingFunction] = React.useState(useSkeletonTimingFunction());
const [loadingDuration, setLoadingDuration] = React.useState(loadingDurationInStore);
const [initTransitionDuration, setInitTransitionDuration] = React.useState(
useSkeletonInitTransitionDuration(),
);

const convertToMs = (duration: string) => {
const [time, unit] = duration.split(/(?<=\d)(?=[a-zA-Z])/);
const timeInMs = Number(time) * (unit === "s" ? 1000 : 1);
return timeInMs;
};

const realLoadingStartTimeLeft = (realLoadingStartTime / loadingDuration) * 100;
const initTransitionDurationLeft =
(convertToMs(initTransitionDuration) / loadingDuration) * 100 + realLoadingStartTimeLeft;

React.useEffect(() => {
let offTimeout: NodeJS.Timeout;
let onTimeout: NodeJS.Timeout;

if (isLoading) {
onTimeout = setTimeout(() => {
setControls({ isRealLoading: true });
}, realLoadingStartTime);

offTimeout = setTimeout(() => {
setControls({ isLoading: false, isRealLoading: false });
}, loadingDuration);
} else {
setControls({ isRealLoading: false });
}

return () => {
clearTimeout(offTimeout);
clearTimeout(onTimeout);
};
}, [isLoading, realLoadingStartTime, loadingDuration, setControls]);

return (
<div>
<BoxButton variant={isLoading ? "brandWeak" : "brandSolid"} onClick={toggleLoading}>
<BoxButton
variant={isLoading ? "brandWeak" : "brandSolid"}
onClick={() => setControls({ isLoading: !isLoading })}
>
{isLoading ? "Stop Loading" : "Start Loading"}
</BoxButton>

<div>
<div className={controlBlock}>
<label className={controlLabel} htmlFor="duration">
<h2 className={css.controlTitle}>Timeline</h2>
<div
style={{
width: "100%",
height: "1px",
background: "black",
position: "relative",
}}
>
{isLoading && (
<div
style={{
position: "absolute",
top: "50%",
width: "10px",
height: "10px",
background: "red",
transform: "translateY(-50%)",
animation: `${css.leftToRight} ${loadingDuration}ms linear`,
}}
/>
)}

{/* realLoadingStartTime Point */}
<div
style={{
position: "absolute",
top: "50%",
width: "10px",
height: "10px",
background: "blue",
transform: "translateY(-50%)",
left: `${realLoadingStartTimeLeft}%`,
}}
>
<span
style={{
position: "absolute",
top: "100%",
left: "-50%",
transform: "translateX(-50%)",
fontSize: "10px",
whiteSpace: "nowrap",
}}
>
실제 로딩 시작 시간: {realLoadingStartTime}ms
</span>
</div>

{/* initTransitionDuration */}
<div
style={{
position: "absolute",
top: "50%",
width: "10px",
height: "10px",
background: "green",
transform: "translateY(-50%)",
left: `${initTransitionDurationLeft}%`,
}}
>
<span
style={{
position: "absolute",
top: "-150%",
left: "-50%",
transform: "translateX(-50%)",
fontSize: "10px",
whiteSpace: "nowrap",
}}
>
스켈레톤 트랜지션: {initTransitionDuration}ms
</span>
</div>
</div>
</div>
<div>
<h2 className={css.controlTitle}>Transition</h2>

<div>isLoading : {`${isLoading}`}</div>
<div>isRealLoading : {`${isRealLoading}`}</div>

<div className={css.controlBlock}>
<label className={css.controlLabel} htmlFor="initTransitionDuration">
InitTransitionDuration
</label>
<input
className={css.controlInput}
id="initTransitionDuration"
type="string"
value={initTransitionDuration}
onChange={(e) => setInitTransitionDuration(e.target.value)}
/>
</div>
<div className={css.controlBlock}>
<label className={css.controlLabel} htmlFor="realLoadingStartTime">
realLoadingStartTime
</label>
<input
className={css.controlInput}
id="realLoadingStartTime"
type="number"
value={realLoadingStartTime}
onChange={(e) => setRealLoadingStartTime(+e.target.value)}
/>
</div>
</div>
<div>
<h2 className={css.controlTitle}>Loading</h2>
<div className={css.controlBlock}>
<label className={css.controlLabel} htmlFor="loadingDuration">
Loading Duration
</label>
<input
className={css.controlInput}
id="loadingDuration"
type="number"
value={loadingDuration}
onChange={(e) => setLoadingDuration(+e.target.value)}
/>
</div>
<p>{loadingDurationInStore}ms 후에 로딩이 멈춥니다.</p>
</div>
<div>
<h2 className={css.controlTitle}>Transition</h2>
<div className={css.controlBlock}>
<label className={css.controlLabel} htmlFor="duration">
Duration
</label>
<input
className={controlInput}
className={css.controlInput}
id="duration"
value={duration}
onChange={(e) => setDuration(e.target.value)}
/>
</div>
<div className={controlBlock}>
<label className={controlLabel} htmlFor="gradient">
<div className={css.controlBlock}>
<label className={css.controlLabel} htmlFor="gradient">
Gradient
</label>
<input
className={controlInput}
className={css.controlInput}
id="gradient"
value={gradient}
onChange={(e) => setGradient(e.target.value)}
/>
</div>
<div className={controlBlock}>
<label className={controlLabel} htmlFor="timing-function">
<div className={css.controlBlock}>
<label className={css.controlLabel} htmlFor="timing-function">
Timing Function
</label>
<input
className={controlInput}
className={css.controlInput}
id="timing-function"
value={timingFunction}
onChange={(e) => setTimingFunction(e.target.value)}
/>
</div>
<BoxButton type="button" onClick={() => setControls({ duration, gradient })}>
적용
</BoxButton>
</div>
<BoxButton
className={css.adapt}
type="button"
onClick={() =>
setControls({
duration,
gradient,
isLoading,
loadingDurationMs: loadingDuration,
timingFunction,
initTransitionDuration,
realLoadingStartTimeMs: realLoadingStartTime,
isRealLoading,
})
}
>
적용
</BoxButton>
</div>
);
};
19 changes: 15 additions & 4 deletions component-docs/src/stores/skeleton.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
import { create } from "zustand";

interface SkeletonState {
loadingDurationMs: number;
initTransitionDuration: string;
isRealLoading: boolean;
realLoadingStartTimeMs: number;
isLoading: boolean;
duration: string;
gradient: string;
timingFunction: string;
actions: {
toggleLoading: () => void;
setControls: (state: { duration: string; gradient: string }) => void;
setControls: (state: Partial<Omit<SkeletonState, "actions">>) => void;
};
}

const useSkeleton = create<SkeletonState>((set) => ({
isLoading: true,
loadingDurationMs: 1000,
initTransitionDuration: "0.2s",
realLoadingStartTimeMs: 200,
isRealLoading: false,
isLoading: false,
duration: "1.5s",
gradient: "linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)",
timingFunction: "ease-in-out",
actions: {
toggleLoading: () => set((state) => ({ isLoading: !state.isLoading })),
setControls: set,
},
}));

export const useSkeletonLoading = () => useSkeleton((state) => state.isLoading);
export const useSkeletonInitTransitionDuration = () =>
useSkeleton((state) => state.initTransitionDuration);
export const useIsRealLoading = () => useSkeleton((state) => state.isRealLoading);
export const useRealLoadingStartTimeMs = () => useSkeleton((state) => state.realLoadingStartTimeMs);
export const useSkeletonActions = () => useSkeleton((state) => state.actions);
export const useSkeletonDuration = () => useSkeleton((state) => state.duration);
export const useSkeletonGradient = () => useSkeleton((state) => state.gradient);
export const useLoadingDurationMs = () => useSkeleton((state) => state.loadingDurationMs);
export const useSkeletonTimingFunction = () => useSkeleton((state) => state.timingFunction);

0 comments on commit 330484e

Please sign in to comment.