feat(web): shared design primitives (IconChip, SectionHeader, EmptyState)

This commit is contained in:
Munkherdene 2026-08-22 22:09:32 +08:00
parent 66d82b8b68
commit e0c07be232
4 changed files with 130 additions and 0 deletions

48
src/ds/EmptyState.tsx Normal file
View file

@ -0,0 +1,48 @@
import { Icon, type IconName } from "./icons";
/** A designed empty / low-data state: a soft icon medallion, a title, and a
* one-line hint. Replaces the bare "Гүйлгээ алга" text so a quiet screen still
* feels intentional. */
export function EmptyState({
icon,
title,
hint,
compact = false,
}: {
icon: IconName;
title: string;
hint?: string;
compact?: boolean;
}) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
gap: 10,
padding: compact ? "20px 16px" : "36px 20px",
}}
>
<span
aria-hidden
style={{
width: 52,
height: 52,
borderRadius: 18,
display: "grid",
placeItems: "center",
color: "var(--seed-color-fg-neutral-muted, #8b8b8b)",
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
}}
>
<Icon name={icon} size={26} />
</span>
<span style={{ fontSize: 15, fontWeight: 700, color: "var(--seed-color-fg-neutral)" }}>{title}</span>
{hint ? (
<span style={{ fontSize: 13, maxWidth: 280, color: "var(--seed-color-fg-neutral-muted, #8b8b8b)" }}>{hint}</span>
) : null}
</div>
);
}

35
src/ds/IconChip.tsx Normal file
View file

@ -0,0 +1,35 @@
import { Icon, type IconName } from "./icons";
/** The rounded, tinted icon square used on every list row (transactions,
* accounts, assets, planner categories, profile menu). One source of truth for
* the chip so all rows share the exact same shape/size. */
export function IconChip({
icon,
tint,
fg,
size = 40,
}: {
icon: IconName;
tint: string;
fg: string;
size?: number;
}) {
const radius = Math.round(size * 0.32);
return (
<span
aria-hidden
style={{
width: size,
height: size,
flexShrink: 0,
borderRadius: radius,
display: "grid",
placeItems: "center",
color: fg,
background: tint,
}}
>
<Icon name={icon} size={Math.round(size * 0.5)} />
</span>
);
}

44
src/ds/SectionHeader.tsx Normal file
View file

@ -0,0 +1,44 @@
import type { ReactNode } from "react";
import { Icon } from "./icons";
/** A section title with an optional trailing action (e.g. a `+` add button).
* Used to head every list block so spacing + type are consistent. */
export function SectionHeader({
title,
action,
onAction,
actionLabel,
}: {
title: string;
action?: ReactNode;
onAction?: () => void;
actionLabel?: string;
}) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
<h2 style={{ margin: 0, fontSize: 17, fontWeight: 700, color: "var(--seed-color-fg-neutral)" }}>{title}</h2>
{onAction ? (
<button
type="button"
onClick={onAction}
aria-label={actionLabel}
style={{
all: "unset",
cursor: "pointer",
width: 32,
height: 32,
borderRadius: 10,
display: "grid",
placeItems: "center",
color: "var(--seed-color-fg-neutral)",
background: "var(--seed-color-bg-neutral-subtle, #eef0f2)",
}}
>
<Icon name="plus" size={18} />
</button>
) : (
action ?? null
)}
</div>
);
}

View file

@ -17,3 +17,6 @@ export { Card } from "./Card";
export type { CardProps } from "./Card"; export type { CardProps } from "./Card";
export * from "./categoryStyle"; export * from "./categoryStyle";
export * from "./icons"; export * from "./icons";
export * from "./IconChip";
export * from "./SectionHeader";
export * from "./EmptyState";