banner · neumorphic · 灵感来源:GOV.UK notification banner / USWDS alert
Notification Banner · 拟态提示条 · 提示条 / Neumorphic
做一个 Notification Banner(拟态提示条):
- 同一份 props 与 role 分配
- 底板同色挤出,语义色只染图标与标题——上色就不是拟态了
- 少了底色通道,严重度靠图标形状 + 文案承担,暗色下更弱
- 零边框,分层全靠外双阴影
用这些 CSS 变量:--color-info-fg --color-success-fg --color-text --color-text-muted --color-text-subtle --color-warning-fg --neuro-bg --neuro-dark --neuro-light
灵感参考:GOV.UK notification banner / USWDS alert--color-info-fg --color-success-fg --color-text --color-text-muted --color-text-subtle --color-warning-fg --neuro-bg --neuro-dark --neuro-light
(无必缺状态)
(尚未接进整页场景)
/r/banner--neumorphic.json
免外壳的 markdown 版:/llms/components/banner--neumorphic.mdrole 分配、结构三段),见玻璃那支的 notesimport { useState, type ReactNode } from 'react';
import { CircleCheck, Info, TriangleAlert, X } from 'lucide-react';
import { Icon } from '~/components/Icon';
interface Props {
/** 严重度:错误与警告共用一支琥珀色(和输入框的错误描边同色) */
tone?: 'info' | 'success' | 'warning';
title: string;
description?: ReactNode;
/** 一个动作:链接或按钮,放正文之后 */
action?: ReactNode;
/** 可关:常驻不等于不能收,但绝不自动消失(决策 17) */
dismissible?: boolean;
className?: string;
}
const TONE = {
info: { icon: Info, role: 'status' as const, fg: 'text-[var(--color-info-fg)]' },
success: { icon: CircleCheck, role: 'status' as const, fg: 'text-[var(--color-success-fg)]' },
warning: { icon: TriangleAlert, role: 'alert' as const, fg: 'text-[var(--color-warning-fg)]' },
};
/**
* 拟态提示条:面板必须是同色挤出的,所以语义色只染图标与标题,
* 不给底板上色——上色就不是拟态了。严重度靠图标形状 + 文案承担。
*/
export function NeumorphicBanner({
tone = 'info',
title,
description,
action,
dismissible = false,
className = '',
}: Props) {
const [hidden, setHidden] = useState(false);
const t = TONE[tone];
if (hidden) return null;
return (
<div
role={t.role}
className={`
flex items-start gap-3 px-4 py-3.5 rounded-2xl border border-transparent
bg-[var(--neuro-bg)]
shadow-[5px_5px_12px_var(--neuro-dark),-5px_-5px_12px_var(--neuro-light)]
${className}
`}
>
<Icon icon={t.icon} size={16} className={`shrink-0 mt-0.5 ${t.fg}`} />
<div className="min-w-0 flex-1">
<p className={`text-sm font-semibold ${t.fg}`}>{title}</p>
{(description || action) && (
<div className="mt-1 text-sm text-[var(--color-text-muted)] leading-relaxed">
{description}
{action && <span className="mt-2 block">{action}</span>}
</div>
)}
</div>
{dismissible && (
<button
type="button"
onClick={() => setHidden(true)}
aria-label={`关闭提示:${title}`}
className="press-feedback shrink-0 grid place-items-center size-6 -mr-1 rounded-full
text-[var(--color-text-subtle)] hover:text-[var(--color-text)] cursor-pointer"
>
<Icon icon={X} size={14} />
</button>
)}
</div>
);
}
src/components/banner/neumorphic/