Function bodies 369 total
Process function · typescript · L109-L131 (23 LOC)src/components/sections/Process.tsx
export function Process() {
return (
<section className="py-[120px] bg-bg-warm max-md:py-20" id="process">
<div className="max-w-[1200px] mx-auto px-20 max-lg:px-10 max-sm:px-5">
<ScrollReveal>
<SectionHeader
tag="이용 방법"
title="간단한 4단계"
desc="복잡한 절차 없이, 신청부터 수거까지 쉽고 빠르게"
center
/>
</ScrollReveal>
<div className="grid grid-cols-4 gap-8 max-lg:grid-cols-2 max-lg:gap-6 max-sm:grid-cols-1 max-sm:gap-4">
{processSteps.map((step, i) => (
<ScrollReveal key={step.num} delay={i * 0.1}>
<ProcessCard step={step} index={i} />
</ScrollReveal>
))}
</div>
</div>
</section>
);
}useCountUp function · typescript · L7-L43 (37 LOC)src/components/sections/TrustBar.tsx
function useCountUp(target: string, duration = 1200) {
const [display, setDisplay] = useState("0");
const [started, setStarted] = useState(false);
const start = useCallback(() => setStarted(true), []);
useEffect(() => {
if (!started) return;
const numMatch = target.match(/[\d.]+/);
if (!numMatch) {
setDisplay(target);
return;
}
const end = parseFloat(numMatch[0]);
const isDecimal = target.includes(".");
const prefix = target.slice(0, numMatch.index);
const suffix = target.slice((numMatch.index ?? 0) + numMatch[0].length);
const startTime = performance.now();
function step(now: number) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = end * eased;
setDisplay(
prefix + (isDecimal ? current.toFixed(1) : Math.round(current).toString()) + suffix,
);
if (progress < 1) requestAnimationstep function · typescript · L28-L37 (10 LOC)src/components/sections/TrustBar.tsx
function step(now: number) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = end * eased;
setDisplay(
prefix + (isDecimal ? current.toFixed(1) : Math.round(current).toString()) + suffix,
);
if (progress < 1) requestAnimationFrame(step);
}StatItem function · typescript · L45-L80 (36 LOC)src/components/sections/TrustBar.tsx
function StatItem({ value, suffix, label }: { value: string; suffix?: string; label: string }) {
const { display, start } = useCountUp(value);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
start();
observer.unobserve(el);
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, [start]);
return (
<div ref={ref} className="text-center">
<div className="text-[36px] font-extrabold tracking-[-1px] leading-none mb-3 flex items-baseline gap-1 justify-center max-md:text-[28px] text-text-primary">
{display}
{suffix && (
<span className="text-[20px] font-bold text-primary max-md:text-[16px]">
{suffix}
</span>
)}
</div>
<div className="text-[14px] text-text-subTrustBar function · typescript · L82-L97 (16 LOC)src/components/sections/TrustBar.tsx
export function TrustBar() {
return (
<div className="bg-bg-warm/60">
<div className="max-w-[1200px] mx-auto px-20 max-lg:px-10 max-sm:px-5 flex items-center justify-center py-14 gap-16 max-md:gap-8 max-md:flex-wrap max-sm:gap-x-8 max-sm:gap-y-6 max-sm:py-10">
{trustStats.map((stat, i) => (
<div key={stat.label} className="contents">
{i > 0 && (
<div className="w-px h-10 bg-border/40 max-sm:hidden" />
)}
<StatItem value={stat.value} suffix={stat.suffix} label={stat.label} />
</div>
))}
</div>
</div>
);
}Splash function · typescript · L8-L125 (118 LOC)src/components/Splash.tsx
export function Splash({ children }: { children: React.ReactNode }) {
const [phase, setPhase] = useState<"check" | "splash" | "done">("check");
useEffect(() => {
if (sessionStorage.getItem(STORAGE_KEY)) {
setPhase("done");
return;
}
// splash 표시
setPhase("splash");
// 0.8s fade-in + 0.3s fade-out = 1.1s 후 완료
const timer = setTimeout(() => {
sessionStorage.setItem(STORAGE_KEY, "1");
setPhase("done");
}, 1100);
return () => clearTimeout(timer);
}, []);
// 초기 체크 중에는 아무것도 안 보여줌 (깜빡임 방지)
if (phase === "check") {
return null;
}
return (
<>
{phase === "splash" && (
<div className="splash-overlay" aria-hidden="true">
<div className="splash-content">
<Image
src="/images/logo.png"
alt="커버링"
width={56}
height={56}
className="splash-logo"
priority
/>
<span className="splashActionButton function · typescript · L49-L84 (36 LOC)src/components/ui/ActionButton.tsx
function ActionButton(
{
actionType = "caution",
hierarchy = "primary",
size = "md",
disabled = false,
children,
className = "",
...rest
},
ref,
) {
const classes = [
"inline-flex items-center justify-center gap-2 font-semibold",
"transition-all duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
"cursor-pointer disabled:cursor-not-allowed",
colorMap[actionType][hierarchy],
sizeStyles[size],
className,
]
.filter(Boolean)
.join(" ");
return (
<button
ref={ref}
type="button"
disabled={disabled}
className={classes}
{...rest}
>
{children}
</button>
);
},Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
InfoCircleIcon function · typescript · L46-L59 (14 LOC)src/components/ui/Banner.tsx
function InfoCircleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="1.5" />
<path
d="M10 9V14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="10" cy="6.5" r="0.75" fill="currentColor" />
</svg>
);
}CheckCircleIcon function · typescript · L61-L74 (14 LOC)src/components/ui/Banner.tsx
function CheckCircleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="1.5" />
<path
d="M6.5 10L9 12.5L13.5 7.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}ErrorCircleIcon function · typescript · L76-L89 (14 LOC)src/components/ui/Banner.tsx
function ErrorCircleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<circle cx="10" cy="10" r="9" stroke="currentColor" strokeWidth="1.5" />
<path
d="M10 6V11"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="10" cy="13.5" r="0.75" fill="currentColor" />
</svg>
);
}WarningTriangleIcon function · typescript · L91-L109 (19 LOC)src/components/ui/Banner.tsx
function WarningTriangleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M10 2L18.66 17H1.34L10 2Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M10 8V12"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<circle cx="10" cy="14.5" r="0.75" fill="currentColor" />
</svg>
);
}CloseSmallIcon function · typescript · L111-L128 (18 LOC)src/components/ui/Banner.tsx
function CloseSmallIcon() {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M14 6L6 14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M6 6L14 14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
}Banner function · typescript · L138-L203 (66 LOC)src/components/ui/Banner.tsx
export function Banner({
variant = "information",
title,
description,
action,
onClose,
className = "",
icon,
}: BannerProps) {
const styles = variantStyles[variant];
const classes = [
"relative flex gap-[--spacing-sm] p-[--spacing-md] rounded-md w-full",
"transition-all duration-200",
styles.container,
className,
]
.filter(Boolean)
.join(" ");
return (
<div className={classes} role="alert">
{/* Icon */}
<div className={`shrink-0 mt-0.5 ${styles.icon}`}>
{icon || defaultIcons[variant]}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
{title && (
<p
className={`text-[14px] leading-[22px] font-semibold ${styles.text}`}
>
{title}
</p>
)}
<p
className={`text-[14px] leading-[22px] font-normal ${styles.text} ${title ? "mt-0.5" : ""}`}
>
{description}
</p>
{action && (
Spinner function · typescript · L41-L66 (26 LOC)src/components/ui/Button.tsx
function Spinner({ className }: { className?: string }) {
return (
<svg
className={"animate-spin " + (className || "")}
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
>
<circle
cx="8"
cy="8"
r="6"
stroke="currentColor"
strokeOpacity="0.25"
strokeWidth="2"
/>
<path
d="M14 8a6 6 0 0 0-6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}Button function · typescript · L70-L122 (53 LOC)src/components/ui/Button.tsx
function Button(
{
variant = "primary",
size = "md",
loading = false,
fullWidth = false,
disabled = false,
href,
children,
className = "",
type = "button",
...rest
},
ref,
) {
const isDisabled = disabled || loading;
const classes = [
"inline-flex items-center justify-center gap-2 font-semibold",
"transition-all duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
"cursor-pointer disabled:cursor-not-allowed",
variantStyles[variant],
sizeStyles[size],
fullWidth ? "w-full" : "",
className,
]
.filter(Boolean)
.join(" ");
/* Link variant */
if (href && !isDisabled) {
return (
<Link href={href} className={classes}>
{loading && <Spinner />}
{children}
</Link>
);
}
return (
<button
ref={ref}
type={type}
Want this analysis on your repo? https://repobility.com/scan/
CasualBanner function · typescript · L26-L158 (133 LOC)src/components/ui/CasualBanner.tsx
export function CasualBanner({
items,
autoPlay = true,
interval = 4000,
className = "",
}: CasualBannerProps) {
const [current, setCurrent] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const [startX, setStartX] = useState(0);
const [translateX, setTranslateX] = useState(0);
const trackRef = useRef<HTMLDivElement>(null);
const autoPlayRef = useRef<ReturnType<typeof setInterval> | null>(null);
const count = items.length;
/* ─── Auto-play ─── */
const stopAutoPlay = useCallback(() => {
if (autoPlayRef.current) {
clearInterval(autoPlayRef.current);
autoPlayRef.current = null;
}
}, []);
const startAutoPlay = useCallback(() => {
if (!autoPlay || count <= 1) return;
stopAutoPlay();
autoPlayRef.current = setInterval(() => {
setCurrent((prev) => (prev + 1) % count);
}, interval);
}, [autoPlay, count, interval, stopAutoPlay]);
useEffect(() => {
startAutoPlay();
return stopAutoPlay;
},CheckIcon function · typescript · L14-L26 (13 LOC)src/components/ui/Checkbox.tsx
function CheckIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M2.5 6L5 8.5L9.5 3.5"
stroke="white"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}IndeterminateIcon function · typescript · L28-L39 (12 LOC)src/components/ui/Checkbox.tsx
function IndeterminateIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M3 6H9"
stroke="white"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
);
}Checkbox function · typescript · L43-L130 (88 LOC)src/components/ui/Checkbox.tsx
function Checkbox(
{
checked = false,
indeterminate = false,
disabled = false,
error = false,
label,
className = "",
...rest
},
ref,
) {
/* Handle indeterminate state on the native input */
const internalRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (internalRef.current) {
internalRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
/* Merge refs */
function setRefs(el: HTMLInputElement | null) {
internalRef.current = el;
if (typeof ref === "function") ref(el);
else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = el;
}
const isActive = checked || indeterminate;
/* Box style */
const boxColor = (() => {
if (disabled && isActive) return "bg-disable-strong border-disable-strong";
if (disabled) return "bg-disable-assistive border-disable-normal";
if (error) return "border-sesetRefs function · typescript · L65-L69 (5 LOC)src/components/ui/Checkbox.tsx
function setRefs(el: HTMLInputElement | null) {
internalRef.current = el;
if (typeof ref === "function") ref(el);
else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = el;
}DeleteIcon function · typescript · L43-L60 (18 LOC)src/components/ui/Chip.tsx
function DeleteIcon() {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
className="shrink-0"
>
<path
d="M4.5 4.5L9.5 9.5M9.5 4.5L4.5 9.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
}Chip function · typescript · L63-L113 (51 LOC)src/components/ui/Chip.tsx
export function Chip({
variant = "outline",
size = "md",
selected = false,
disabled = false,
onDelete,
children,
className = "",
onClick,
...rest
}: ChipProps) {
const style = variantStyles[variant];
const classes = [
"inline-flex items-center justify-center gap-1 font-medium",
"rounded-full whitespace-nowrap",
"transition-all duration-200",
"cursor-pointer",
sizeStyles[size],
selected && variant === "outline" ? style.selected : style.base,
disabled ? "opacity-40 cursor-not-allowed" : "",
className,
]
.filter(Boolean)
.join(" ");
return (
<button
type="button"
disabled={disabled}
className={classes}
onClick={onClick}
{...rest}
>
<span>{children}</span>
{onDelete && (
<span
role="button"
tabIndex={-1}
onClick={(e) => {
e.stopPropagation();
if (!disabled) onDelete();
}}
className="inline-fleMinusIcon function · typescript · L16-L27 (12 LOC)src/components/ui/Counter.tsx
function MinusIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M3.5 8H12.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
PlusIcon function · typescript · L29-L40 (12 LOC)src/components/ui/Counter.tsx
function PlusIcon() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M8 3.5V12.5M3.5 8H12.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
);
}Counter function · typescript · L44-L144 (101 LOC)src/components/ui/Counter.tsx
function Counter(
{
value,
onChange,
min = 0,
max = 99,
disabled = false,
label,
className = "",
...rest
},
ref,
) {
const atMin = value <= min;
const atMax = value >= max;
function decrement() {
if (!atMin && !disabled) onChange(value - 1);
}
function increment() {
if (!atMax && !disabled) onChange(value + 1);
}
const btnBase = [
"flex items-center justify-center",
"h-8 w-8 rounded-sm border",
"transition-all duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
].join(" ");
const btnEnabled =
"border-border text-text-sub hover:bg-bg-warm cursor-pointer";
const btnDisabled =
"border-disable-alt text-disable-strong cursor-not-allowed";
return (
<div className="flex flex-col">
{/* Label */}
{label && (
<label className="mb-2 text-sm font-semibold decrement function · typescript · L60-L62 (3 LOC)src/components/ui/Counter.tsx
function decrement() {
if (!atMin && !disabled) onChange(value - 1);
}increment function · typescript · L64-L66 (3 LOC)src/components/ui/Counter.tsx
function increment() {
if (!atMax && !disabled) onChange(value + 1);
}CTALink function · typescript · L13-L25 (13 LOC)src/components/ui/CTALink.tsx
export function CTALink({ location, children, className }: Props) {
return (
<a
href={KAKAO_CHAT_URL}
target="_blank"
rel="noopener noreferrer"
className={className}
onClick={() => track("cta_click", { location })}
>
{children}
</a>
);
}Divider function · typescript · L20-L34 (15 LOC)src/components/ui/Divider.tsx
export function Divider({ height = "xs", className = "" }: DividerProps) {
return (
<div
role="separator"
className={[
"w-full shrink-0",
"transition-all duration-200",
heightStyles[height],
className,
]
.filter(Boolean)
.join(" ")}
/>
);
}DotBadge function · typescript · L27-L45 (19 LOC)src/components/ui/DotBadge.tsx
export function DotBadge({
size = "sm",
color = "primary",
className = "",
}: DotBadgeProps) {
return (
<span
className={[
"absolute top-0 right-0 block rounded-full",
"transition-all duration-200",
sizeStyles[size],
colorStyles[color],
className,
]
.filter(Boolean)
.join(" ")}
/>
);
}ChevronDown function · typescript · L25-L43 (19 LOC)src/components/ui/Dropdown.tsx
function ChevronDown({ className }: { className?: string }) {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
className={className}
>
<path
d="M5 7.5L10 12.5L15 7.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}Repobility · code-quality intelligence · https://repobility.com
Dropdown function · typescript · L47-L207 (161 LOC)src/components/ui/Dropdown.tsx
function Dropdown(
{
label,
options,
value,
onChange,
placeholder = "Select...",
disabled = false,
error = false,
helperText,
className = "",
name,
},
ref,
) {
const [open, setOpen] = useState(false);
const [focused, setFocused] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
/* Close on outside click */
useEffect(() => {
if (!open) return;
function handleClick(e: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [open]);
const selectedOption = options.find((o) => o.value === value);
/* Border color by state */
const borderColor = (() => {
if (disabled) return "border-disablehandleClick function · typescript · L69-L76 (8 LOC)src/components/ui/Dropdown.tsx
function handleClick(e: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}ChevronLeftIcon function · typescript · L16-L34 (19 LOC)src/components/ui/GeneralHeader.tsx
function ChevronLeftIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15 19L8 12L15 5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}GeneralHeader function · typescript · L37-L85 (49 LOC)src/components/ui/GeneralHeader.tsx
export function GeneralHeader({
title,
onBack,
rightAction,
className = "",
sticky = false,
transparent = false,
}: GeneralHeaderProps) {
const baseClasses = [
"flex items-center h-14 px-[--spacing-md] gap-[--spacing-xs]",
"transition-all duration-200",
];
const stickyClasses = sticky
? "fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-border-light"
: "";
const bgClasses = transparent ? "bg-transparent" : sticky ? "" : "bg-white";
const classes = [...baseClasses, stickyClasses, bgClasses, className]
.filter(Boolean)
.join(" ");
return (
<header className={classes}>
{/* Back button */}
{onBack && (
<button
type="button"
onClick={onBack}
className="inline-flex items-center justify-center w-10 h-10 -ml-2 rounded-full text-text-primary hover:bg-bg-warm2 transition-all duration-200 cursor-pointer shrink-0"
aria-label="뒤로가기"
>
<ChevIconButton function · typescript · L36-L74 (39 LOC)src/components/ui/IconButton.tsx
function IconButton(
{
variant = "tertiary",
size = "md",
shape = "circle",
disabled = false,
children,
className = "",
...rest
},
ref,
) {
const radiusClass = shape === "circle" ? "rounded-full" : "rounded-md";
const classes = [
"inline-flex items-center justify-center shrink-0",
"transition-all duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
"cursor-pointer disabled:cursor-not-allowed",
variantStyles[variant],
sizeStyles[size],
radiusClass,
className,
]
.filter(Boolean)
.join(" ");
return (
<button
ref={ref}
type="button"
disabled={disabled}
className={classes}
{...rest}
>
{children}
</button>
);
},IndicatorDot function · typescript · L12-L54 (43 LOC)src/components/ui/IndicatorDot.tsx
export function IndicatorDot({
total,
current,
className = "",
onChange,
}: IndicatorDotProps) {
return (
<div
className={[
"inline-flex items-center gap-2",
className,
]
.filter(Boolean)
.join(" ")}
role="tablist"
>
{Array.from({ length: total }, (_, i) => {
const isActive = i === current;
return (
<button
key={i}
type="button"
role="tab"
aria-selected={isActive}
aria-label={`${i + 1} / ${total}`}
onClick={() => onChange?.(i)}
className={[
"block rounded-full shrink-0",
"transition-all duration-200 ease-out",
isActive
? "w-4 h-1.5 bg-brand-400"
: "w-1.5 h-1.5 bg-border hover:bg-fill-assistive",
onChange ? "cursor-pointer" : "cursor-default",
]
.filter(Boolean)
.join(" ")}
CheckIcon function · typescript · L13-L30 (18 LOC)src/components/ui/IndicatorNumber.tsx
function CheckIcon() {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
>
<path
d="M3.5 7L6 9.5L10.5 4.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}IndicatorNumber function · typescript · L33-L96 (64 LOC)src/components/ui/IndicatorNumber.tsx
export function IndicatorNumber({
step,
total,
current,
label,
className = "",
}: IndicatorNumberProps) {
const isCompleted = step < current;
const isActive = step === current;
/* const isUpcoming = step > current; */
/* Circle style */
const circleStyle = (() => {
if (isCompleted) return "bg-brand-400 text-white";
if (isActive) return "bg-brand-400 text-white";
return "bg-[#DEE3ED] text-[#8A96A8]";
})();
/* Label style */
const labelStyle = (() => {
if (isActive) return "text-brand-700 font-semibold";
if (isCompleted) return "text-text-sub";
return "text-[#8A96A8]";
})();
return (
<div
className={[
"inline-flex flex-col items-center gap-1.5",
"transition-all duration-200",
className,
]
.filter(Boolean)
.join(" ")}
aria-label={`Step ${step} of ${total}${label ? `: ${label}` : ""}`}
aria-current={isActive ? "step" : undefined}
>
{/* Number circle */}
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
KakaoIcon function · typescript · L1-L12 (12 LOC)src/components/ui/KakaoIcon.tsx
export function KakaoIcon({ size = 20 }: { size?: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M10 1C5.03 1 1 4.13 1 8c0 2.39 1.56 4.49 3.92 5.73-.12.44-.78 2.85-.81 3.04 0 0-.02.15.07.21.1.06.21.01.21.01.28-.04 3.24-2.1 3.75-2.46.28.04.57.06.86.06 4.97 0 9-3.13 9-7 0-3.87-4.03-7-9-7z" />
</svg>
);
}Label function · typescript · L41-L62 (22 LOC)src/components/ui/Label.tsx
export function Label({
variant = "primary",
size = "md",
children,
className = "",
}: LabelProps) {
return (
<span
className={[
"inline-flex items-center justify-center font-semibold whitespace-nowrap",
"transition-all duration-200",
variantStyles[variant],
sizeStyles[size],
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</span>
);
}LinkButton function · typescript · L25-L72 (48 LOC)src/components/ui/LinkButton.tsx
function LinkButton(
{
size = "md",
disabled = false,
href,
icon,
children,
className = "",
...rest
},
ref,
) {
const classes = [
"inline-flex items-center gap-1 font-semibold",
"text-brand-400 hover:text-brand-300",
"underline-offset-2 hover:underline",
"transition-all duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
"cursor-pointer",
disabled ? "opacity-40 pointer-events-none" : "",
sizeStyles[size],
className,
]
.filter(Boolean)
.join(" ");
if (href && !disabled) {
return (
<Link href={href} className={classes}>
{children}
{icon && <span className="inline-flex shrink-0">{icon}</span>}
</Link>
);
}
return (
<button
ref={ref}
type="button"
disabled={disabled}
className={classes}
{...rest}
LoadingSpinner function · typescript · L28-L76 (49 LOC)src/components/ui/LoadingSpinner.tsx
export function LoadingSpinner({
size = "md",
color = "primary",
className = "",
}: LoadingSpinnerProps) {
const px = sizeMap[size];
const stroke = colorMap[color];
const r = px / 2 - 2;
const circumference = 2 * Math.PI * r;
return (
<svg
width={px}
height={px}
viewBox={`0 0 ${px} ${px}`}
fill="none"
className={[
"animate-spin",
"transition-all duration-200",
className,
]
.filter(Boolean)
.join(" ")}
style={{ animationDuration: "0.8s" }}
>
{/* Background track */}
<circle
cx={px / 2}
cy={px / 2}
r={r}
stroke={stroke}
strokeOpacity="0.2"
strokeWidth="2.5"
fill="none"
/>
{/* Active arc */}
<circle
cx={px / 2}
cy={px / 2}
r={r}
stroke={stroke}
strokeWidth="2.5"
strokeLinecap="round"
strokeDasharray={`${circumference * 0.7} ${circumference * 0.MessageBox function · typescript · L40-L83 (44 LOC)src/components/ui/MessageBox.tsx
export function MessageBox({
variant = "received",
children,
timestamp,
className = "",
}: MessageBoxProps) {
const styles = variantStyles[variant];
const wrapperClasses = [
styles.wrapper,
"transition-all duration-200",
className,
]
.filter(Boolean)
.join(" ");
const isSystem = variant === "system";
return (
<div className={wrapperClasses}>
<div className={`max-w-[70%] ${isSystem ? "max-w-full" : ""}`}>
{/* Bubble */}
<div className={styles.bubble}>
<div
className={
isSystem
? "text-[12px] leading-[18px] text-center"
: "text-[14px] leading-[22px]"
}
>
{children}
</div>
</div>
{/* Timestamp */}
{timestamp && !isSystem && (
<p className={`text-[11px] leading-[16px] mt-1 ${styles.time}`}>
{timestamp}
</p>
)}
</div>
</div>
);
}MessageItem function · typescript · L17-L80 (64 LOC)src/components/ui/MessageItem.tsx
export function MessageItem({
icon,
title,
description,
timestamp,
unread = false,
onClick,
className = "",
}: MessageItemProps) {
const Tag = onClick ? "button" : "div";
const classes = [
"relative flex flex-row w-full px-[--spacing-lg] py-[--spacing-md] gap-[--spacing-sm]",
"transition-all duration-200",
onClick ? "cursor-pointer hover:bg-bg-warm active:bg-bg-warm2 text-left" : "",
className,
]
.filter(Boolean)
.join(" ");
return (
<Tag
{...(onClick ? { type: "button" as const, onClick } : {})}
className={classes}
>
{/* Icon */}
{icon !== undefined && (
<div className="relative shrink-0">
<div className="w-10 h-10 rounded-full bg-bg-warm2 flex items-center justify-center">
{icon}
</div>
{/* Unread dot */}
{unread && (
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-brand-400" />
)}
</div>
CloseIcon function · typescript · L12-L37 (26 LOC)src/components/ui/ModalHeader.tsx
function CloseIcon() {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6L6 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M6 6L18 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}ModalHeader function · typescript · L40-L82 (43 LOC)src/components/ui/ModalHeader.tsx
export function ModalHeader({
title,
onClose,
subtitle,
className = "",
}: ModalHeaderProps) {
const classes = [
"flex items-center justify-between h-14 px-[--spacing-lg]",
"border-b border-[#DEE3ED]",
"transition-all duration-200",
className,
]
.filter(Boolean)
.join(" ");
return (
<header className={classes}>
{/* Title group */}
<div className="flex flex-col min-w-0">
<h2 className="text-[18px] leading-[26px] font-bold text-text-primary truncate">
{title}
</h2>
{subtitle && (
<p className="text-[14px] leading-[22px] font-normal text-text-sub truncate">
{subtitle}
</p>
)}
</div>
{/* Close button */}
{onClose && (
<button
type="button"
onClick={onClose}
className="inline-flex items-center justify-center w-10 h-10 -mr-2 rounded-full text-text-muted hover:text-text-primary hover:bg-bg-warm2 transition-allWant this analysis on your repo? https://repobility.com/scan/
Chevron function · typescript · L28-L47 (20 LOC)src/components/ui/NavButton.tsx
function Chevron({ direction, size }: { direction: Direction; size: number }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{direction === "left" ? (
<path d="M15 18l-6-6 6-6" />
) : (
<path d="M9 18l6-6-6-6" />
)}
</svg>
);
}NavButton function · typescript · L51-L87 (37 LOC)src/components/ui/NavButton.tsx
function NavButton(
{
direction = "left",
size = "md",
disabled = false,
className = "",
...rest
},
ref,
) {
const classes = [
"inline-flex items-center justify-center shrink-0 rounded-full",
"bg-white border border-border text-text-primary",
"hover:bg-bg-warm2",
"transition-all duration-200",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-400",
"cursor-pointer disabled:cursor-not-allowed disabled:opacity-40",
"shadow-sm",
sizeStyles[size],
className,
]
.filter(Boolean)
.join(" ");
return (
<button
ref={ref}
type="button"
disabled={disabled}
className={classes}
aria-label={direction === "left" ? "Previous" : "Next"}
{...rest}
>
<Chevron direction={direction} size={iconSizes[size]} />
</button>
);
},NumberBadge function · typescript · L28-L55 (28 LOC)src/components/ui/NumberBadge.tsx
export function NumberBadge({
count,
maxCount = 99,
size = "sm",
variant = "primary",
className = "",
}: NumberBadgeProps) {
if (count <= 0) return null;
const display = count > maxCount ? `${maxCount}+` : String(count);
return (
<span
className={[
"inline-flex items-center justify-center px-1 font-bold leading-none",
"rounded-full",
"transition-all duration-200",
sizeStyles[size],
variantStyles[variant],
className,
]
.filter(Boolean)
.join(" ")}
>
{display}
</span>
);
}