feat: 기능 추가 과정중

This commit is contained in:
donghyeon-ka
2026-07-30 15:58:20 +09:00
parent d3ef801fe6
commit 6c52cdb916
648 changed files with 126325 additions and 6680 deletions
@@ -0,0 +1,144 @@
import { useId, type ReactNode } from "react";
import type { AsyncState } from "../../application/view-models/async-state.ts";
import type { AppFailure } from "../../contracts/errors.ts";
import { useLocale } from "../i18n/index.ts";
import { errorMessage } from "./error-copy.ts";
import { Button } from "./ui/button.ts";
export function LoadingSurface({ label }: Readonly<{ label?: string }>) {
const { message } = useLocale();
const accessibleLabel = label ?? message("async.loading");
return (
<section
className="state-surface state-surface--loading"
aria-busy="true"
aria-label={accessibleLabel}
aria-live="polite"
aria-atomic="true"
>
<div className="ui-skeleton" aria-hidden="true" />
<span className="visually-hidden">{accessibleLabel}</span>
</section>
);
}
export type EmptySurfaceProps = Readonly<{
title?: string;
description?: string;
action?: ReactNode;
}>;
export function EmptySurface({
title,
description,
action,
}: EmptySurfaceProps) {
const { message } = useLocale();
return (
<section className="ui-empty state-surface" aria-live="polite">
<h2>{title ?? message("async.empty")}</h2>
{description ? <p>{description}</p> : null}
{action}
</section>
);
}
export type TerminalErrorSurfaceProps = Readonly<{
userMessageKey: string;
action: AppFailure["action"];
onAction?: () => void;
}>;
export function TerminalErrorSurface({
userMessageKey,
action,
onAction,
}: TerminalErrorSurfaceProps) {
const { locale, message } = useLocale();
const messageId = useId();
const actionLabels: Readonly<
Record<Exclude<AppFailure["action"], "none">, string>
> = Object.freeze({
retry: message("action.retry"),
reauth: message("action.reauth"),
navigate: message("action.navigateSafe"),
"reload-once": message("action.reloadOnce"),
"contact-support": message("action.contactSupport"),
});
return (
<section
className="ui-terminal-error state-surface state-surface--danger"
role="alert"
aria-labelledby={messageId}
data-message-key={userMessageKey}
>
<h2 id={messageId}>{errorMessage(userMessageKey, locale)}</h2>
{action !== "none" && onAction && (
<Button onClick={onAction}>{actionLabels[action]}</Button>
)}
</section>
);
}
export type AsyncSurfaceProps = Readonly<{
state: AsyncState;
children?: ReactNode;
onAction?: () => void;
onRetry?: () => void;
onResolveConflict?: () => void;
}>;
export function AsyncSurface({
state,
children,
onAction,
onRetry,
onResolveConflict,
}: AsyncSurfaceProps) {
const { message } = useLocale();
if (state.base === "initial-loading") return <LoadingSurface />;
if (state.base === "empty") return <EmptySurface />;
if (state.base === "terminal-error" && state.failure) {
return (
<TerminalErrorSurface
userMessageKey={state.failure.userMessageKey}
action={state.failure.action}
onAction={
state.failure.action === "retry"
? onRetry ?? onAction
: onAction
}
/>
);
}
return (
<section aria-busy={state.overlay.refreshing || state.overlay.mutationPending}>
{state.indicator ? (
<div role="status" aria-live="polite">
<span>
{message(
state.indicator === "stale-degraded"
? "async.staleDegraded"
: state.indicator === "mutation-conflict"
? "async.mutationConflict"
: state.indicator === "mutation-pending"
? "async.mutationPending"
: "async.refreshing",
)}
</span>
{state.indicator === "stale-degraded" && onRetry ? (
<Button onClick={onRetry}>{message("action.retry")}</Button>
) : null}
{state.indicator === "mutation-conflict" && onResolveConflict ? (
<Button onClick={onResolveConflict}>
{message("action.resolveConflict")}
</Button>
) : null}
</div>
) : null}
{children}
</section>
);
}