feat(asset-center): add light animated empty workbench model

This commit is contained in:
WuYongJie 2026-04-25 19:07:21 +08:00
parent 093d8a215c
commit 22c983ab16
2 changed files with 298 additions and 113 deletions

View File

@ -4,26 +4,25 @@ interface AssetWorkbenchEmptyBackdropProps {
className?: string
}
const componentPlates = [
'left-[18%] top-[28%] h-[86px] w-[156px] -rotate-6',
'right-[18%] top-[24%] h-[70px] w-[126px] rotate-3',
'left-[30%] bottom-[20%] h-[78px] w-[138px] rotate-2',
'right-[28%] bottom-[27%] h-[56px] w-[104px] -rotate-3',
const modelBoards = [
'asset-empty-model-board asset-empty-model-board-main',
'asset-empty-model-board asset-empty-model-board-back',
'asset-empty-model-board asset-empty-model-board-side-left',
'asset-empty-model-board asset-empty-model-board-side-right',
]
const hardwareNodes = [
'left-[23%] top-[34%]',
'left-[39%] top-[42%]',
'right-[24%] top-[31%]',
'left-[34%] bottom-[27%]',
'right-[33%] bottom-[36%]',
'right-[20%] bottom-[25%]',
const modelLinks = [
'asset-empty-model-link asset-empty-model-link-a',
'asset-empty-model-link asset-empty-model-link-b',
'asset-empty-model-link asset-empty-model-link-c',
]
const relationLines = [
'left-[28%] top-[39%] w-[18%] rotate-[13deg]',
'left-[45%] top-[43%] w-[22%] -rotate-[11deg]',
'left-[38%] bottom-[34%] w-[25%] rotate-[7deg]',
const modelNodes = [
'asset-empty-model-node asset-empty-model-node-a',
'asset-empty-model-node asset-empty-model-node-b',
'asset-empty-model-node asset-empty-model-node-c',
'asset-empty-model-node asset-empty-model-node-d',
'asset-empty-model-node asset-empty-model-node-e',
]
export function AssetWorkbenchEmptyBackdrop({ className }: AssetWorkbenchEmptyBackdropProps) {
@ -32,106 +31,283 @@ export function AssetWorkbenchEmptyBackdrop({ className }: AssetWorkbenchEmptyBa
aria-hidden="true"
data-testid="asset-workbench-empty-backdrop"
className={cn(
'pointer-events-none absolute inset-0 isolate overflow-hidden bg-[#0c0e10] text-white',
'pointer-events-none absolute inset-0 isolate overflow-hidden bg-[#f4f6f6] text-[#2d3335]',
className,
)}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_42%,rgba(172,179,184,0.12),transparent_42%),linear-gradient(180deg,rgba(255,255,255,0.035),transparent_34%,rgba(255,255,255,0.018))]" />
<div className="absolute inset-0 opacity-60 bg-[linear-gradient(rgba(220,227,233,0.055)_1px,transparent_1px),linear-gradient(90deg,rgba(220,227,233,0.055)_1px,transparent_1px)] bg-[size:44px_44px]" />
<div className="absolute inset-0 opacity-40 bg-[linear-gradient(rgba(191,216,229,0.045)_1px,transparent_1px),linear-gradient(90deg,rgba(191,216,229,0.045)_1px,transparent_1px)] bg-[size:176px_176px]" />
<div className="absolute inset-x-[14%] top-[22%] h-px bg-gradient-to-r from-transparent via-white/12 to-transparent" />
<div className="absolute inset-x-[20%] bottom-[21%] h-px bg-gradient-to-r from-transparent via-[#bfd8e5]/14 to-transparent" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_46%,rgba(126,145,149,0.14),transparent_40%),linear-gradient(180deg,rgba(255,255,255,0.76),transparent_42%,rgba(223,229,230,0.30))]" />
<div className="absolute inset-0 opacity-45 bg-[linear-gradient(rgba(71,100,107,0.052)_1px,transparent_1px),linear-gradient(90deg,rgba(71,100,107,0.052)_1px,transparent_1px)] bg-[size:56px_56px]" />
<div className="absolute inset-x-[18%] top-[27%] h-px bg-gradient-to-r from-transparent via-[#47646b]/12 to-transparent" />
<div className="absolute inset-x-[24%] bottom-[25%] h-px bg-gradient-to-r from-transparent via-[#47646b]/10 to-transparent" />
<div className="absolute left-1/2 top-1/2 h-[44%] w-[52%] -translate-x-1/2 -translate-y-1/2 rounded-[8px] border border-white/[0.07] bg-white/[0.015] shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]" />
<div className="absolute left-1/2 top-1/2 h-[30%] w-[38%] -translate-x-1/2 -translate-y-1/2 rounded-[8px] border border-dashed border-[#bfd8e5]/18" />
{relationLines.map(lineClassName => (
<div data-testid="low-noise-model-scene" className="asset-empty-model-scene">
<div data-testid="low-noise-model" className="asset-empty-model">
<span className="asset-empty-model-shadow" />
{modelLinks.map(linkClassName => (
<span
key={lineClassName}
data-testid="workbench-relation-line"
className={cn('absolute h-px origin-left bg-gradient-to-r from-transparent via-[#bfd8e5]/24 to-transparent', lineClassName)}
key={linkClassName}
data-testid="low-noise-model-link"
className={linkClassName}
/>
))}
<div
data-testid="packaging-wireframe"
className="absolute left-1/2 top-[48%] h-[168px] w-[252px] -translate-x-1/2 -translate-y-1/2 rounded-[8px] border border-[#bfd8e5]/18 bg-[#bfd8e5]/[0.018]"
>
<span className="absolute left-5 top-5 h-[58px] w-[88px] rounded-[4px] border border-white/10" />
<span className="absolute bottom-5 right-6 h-[62px] w-[104px] rounded-[4px] border border-white/10" />
<span className="absolute left-1/2 top-0 h-full w-px bg-white/[0.055]" />
<span className="absolute left-0 top-1/2 h-px w-full bg-white/[0.055]" />
</div>
{componentPlates.map(plateClassName => (
<div
key={plateClassName}
data-testid="meta-component-plate"
className={cn(
'absolute rounded-[6px] border border-white/[0.09] bg-[#dde3e9]/[0.045] shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]',
plateClassName,
)}
>
<span className="absolute left-3 top-3 h-1.5 w-8 rounded-full bg-[#bfd8e5]/25" />
<span className="absolute bottom-3 left-3 h-1 w-12 rounded-full bg-white/10" />
<span className="absolute right-3 top-3 h-5 w-5 rounded-[4px] border border-white/10" />
</div>
{modelBoards.map(boardClassName => (
<span
key={boardClassName}
data-testid="low-noise-model-board"
className={boardClassName}
/>
))}
{hardwareNodes.map(nodeClassName => (
{modelNodes.map(nodeClassName => (
<span
key={nodeClassName}
data-testid="hardware-node"
className={cn(
'absolute h-4 w-4 rounded-full border border-[#bfd8e5]/26 bg-[#0c0e10] shadow-[0_0_0_4px_rgba(191,216,229,0.04)]',
nodeClassName,
)}
>
<span className="absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#bfd8e5]/42" />
</span>
data-testid="low-noise-model-node"
className={nodeClassName}
/>
))}
<div
data-testid="workbench-hud"
className="absolute left-8 top-8 w-[184px] rounded-[8px] bg-zinc-950/64 p-3 text-[10px] text-white/58 shadow-sm ring-1 ring-white/10 backdrop-blur-2xl"
>
<div className="flex items-center justify-between gap-4">
<span className="font-apple-mono uppercase tracking-apple-normal">META COMPONENT</span>
<span className="h-1.5 w-1.5 rounded-full bg-[#bfd8e5]/50" />
</div>
<div className="mt-2 h-px bg-white/10" />
<div className="mt-2 grid grid-cols-3 gap-1">
<span className="h-1 rounded-full bg-white/20" />
<span className="h-1 rounded-full bg-white/12" />
<span className="h-1 rounded-full bg-white/16" />
<span data-testid="low-noise-model-scan" className="asset-empty-model-scan" />
</div>
</div>
<div
data-testid="workbench-hud"
className="absolute bottom-8 right-8 w-[156px] rounded-[8px] bg-zinc-950/64 p-3 shadow-sm ring-1 ring-white/10 backdrop-blur-2xl"
>
<div className="grid grid-cols-4 gap-1.5">
<span className="h-6 rounded-[4px] border border-white/10 bg-white/[0.035]" />
<span className="h-6 rounded-[4px] border border-white/10 bg-white/[0.055]" />
<span className="h-6 rounded-[4px] border border-white/10 bg-white/[0.03]" />
<span className="h-6 rounded-[4px] border border-white/10 bg-[#bfd8e5]/[0.08]" />
</div>
<div className="mt-2 flex items-center justify-between text-[9px] font-apple-mono uppercase tracking-apple-normal text-white/45">
<span>4D LINK</span>
<span>READY</span>
</div>
</div>
<div className="absolute left-[7%] top-[11%] h-10 w-[160px] rounded-[8px] border border-[#47646b]/10 bg-white/46 shadow-[inset_0_1px_0_rgba(255,255,255,0.72)] backdrop-blur-xl" />
<div className="absolute right-[17%] top-[29%] h-8 w-[112px] rounded-[8px] border border-[#47646b]/10 bg-white/34 opacity-70 backdrop-blur-xl" />
<div className="absolute bottom-[16%] left-[12%] h-7 w-[132px] rounded-[8px] border border-[#47646b]/10 bg-white/32 opacity-65 backdrop-blur-xl" />
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-[#f4f6f6] to-transparent" />
<div
data-testid="workbench-hud"
className="absolute bottom-8 left-8 flex items-center gap-2 rounded-[8px] bg-zinc-950/64 px-3 py-2 text-[10px] font-apple-mono uppercase tracking-apple-normal text-white/45 shadow-sm ring-1 ring-white/10 backdrop-blur-2xl"
>
<span className="h-1.5 w-8 rounded-full bg-[#bfd8e5]/30" />
<span>ATELIER GRID</span>
</div>
<style jsx>{`
.asset-empty-model-scene {
position: absolute;
inset: 0;
perspective: 1200px;
perspective-origin: 50% 46%;
}
<div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-[#0c0e10] to-transparent" />
.asset-empty-model {
position: absolute;
left: 50%;
top: 48%;
width: 560px;
height: 320px;
transform-style: preserve-3d;
transform: translate3d(-52%, -49%, 0) rotateX(58deg) rotateZ(-24deg);
animation: asset-empty-model-float 8.5s ease-in-out infinite;
}
.asset-empty-model-shadow {
position: absolute;
left: 50%;
top: 52%;
width: 380px;
height: 150px;
border-radius: 999px;
background: radial-gradient(ellipse at center, rgba(45, 51, 53, 0.12), transparent 68%);
filter: blur(22px);
transform: translate3d(-50%, -50%, -42px);
}
.asset-empty-model-board,
.asset-empty-model-link,
.asset-empty-model-node,
.asset-empty-model-scan {
position: absolute;
display: block;
transform-style: preserve-3d;
}
.asset-empty-model-board {
border: 1px solid rgba(71, 100, 107, 0.16);
border-radius: 8px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.88), rgba(156, 174, 179, 0.34)),
linear-gradient(90deg, rgba(255, 255, 255, 0.58), transparent 46%, rgba(255, 255, 255, 0.34));
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.72),
0 18px 55px rgba(45, 51, 53, 0.10);
overflow: hidden;
}
.asset-empty-model-board::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.50), transparent),
repeating-linear-gradient(90deg, rgba(71, 100, 107, 0.055) 0 1px, transparent 1px 34px);
opacity: 0.48;
}
.asset-empty-model-board::after {
content: '';
position: absolute;
left: 10%;
right: 10%;
bottom: 14px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(71, 100, 107, 0.28), transparent);
}
.asset-empty-model-board-main {
left: 50%;
top: 50%;
width: 360px;
height: 150px;
transform: translate3d(-50%, -50%, 8px);
}
.asset-empty-model-board-back {
left: 50%;
top: 50%;
width: 318px;
height: 118px;
opacity: 0.50;
transform: translate3d(-50%, -65%, 62px) rotateX(-62deg);
}
.asset-empty-model-board-side-left {
left: 50%;
top: 50%;
width: 118px;
height: 104px;
opacity: 0.42;
transform: translate3d(-214px, -46px, 44px) rotateY(68deg);
}
.asset-empty-model-board-side-right {
left: 50%;
top: 50%;
width: 104px;
height: 96px;
opacity: 0.38;
transform: translate3d(116px, -54px, 50px) rotateY(-66deg);
}
.asset-empty-model-link {
height: 1px;
width: 190px;
background: linear-gradient(90deg, transparent, rgba(71, 100, 107, 0.24), transparent);
transform-origin: left center;
}
.asset-empty-model-link-a {
left: 150px;
top: 114px;
transform: translateZ(38px) rotate(16deg);
}
.asset-empty-model-link-b {
left: 260px;
top: 182px;
transform: translateZ(40px) rotate(-21deg);
}
.asset-empty-model-link-c {
left: 198px;
top: 229px;
width: 230px;
transform: translateZ(22px) rotate(4deg);
}
.asset-empty-model-node {
width: 14px;
height: 14px;
border-radius: 999px;
border: 1px solid rgba(71, 100, 107, 0.20);
background: rgba(255, 255, 255, 0.82);
box-shadow:
0 0 0 5px rgba(71, 100, 107, 0.045),
0 0 22px rgba(71, 100, 107, 0.10);
}
.asset-empty-model-node::after {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 4px;
height: 4px;
border-radius: 999px;
background: rgba(71, 100, 107, 0.42);
transform: translate(-50%, -50%);
}
.asset-empty-model-node-a {
left: 152px;
top: 116px;
transform: translateZ(70px);
}
.asset-empty-model-node-b {
left: 276px;
top: 84px;
transform: translateZ(92px);
}
.asset-empty-model-node-c {
left: 372px;
top: 152px;
transform: translateZ(76px);
}
.asset-empty-model-node-d {
left: 224px;
top: 232px;
transform: translateZ(54px);
}
.asset-empty-model-node-e {
left: 430px;
top: 218px;
transform: translateZ(50px);
}
.asset-empty-model-scan {
left: 102px;
top: 86px;
width: 356px;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(71, 100, 107, 0.34), transparent);
filter: blur(0.2px);
opacity: 0;
transform: translateZ(96px);
animation: asset-empty-scan 5.6s ease-in-out infinite;
}
@keyframes asset-empty-model-float {
0%,
100% {
transform: translate3d(-52%, -49%, 0) rotateX(58deg) rotateZ(-24deg);
}
50% {
transform: translate3d(-52%, -52%, 0) rotateX(58deg) rotateZ(-24deg);
}
}
@keyframes asset-empty-scan {
0%,
22% {
opacity: 0;
transform: translate3d(0, -72px, 96px);
}
38%,
58% {
opacity: 0.44;
}
78%,
100% {
opacity: 0;
transform: translate3d(0, 98px, 96px);
}
}
@media (prefers-reduced-motion: reduce) {
.asset-empty-model,
.asset-empty-model-scan {
animation: none;
}
}
`}</style>
</div>
)
}

View File

@ -20,14 +20,23 @@ describe('AssetWorkbenchEmptyBackdrop', () => {
expect(container.querySelector('picture')).not.toBeInTheDocument()
})
it('contains quiet meta component, hardware, packaging, and HUD elements', () => {
it('contains a quiet low-noise model scene with simple motion primitives', () => {
render(<AssetWorkbenchEmptyBackdrop />)
expect(screen.getAllByTestId('meta-component-plate')).toHaveLength(4)
expect(screen.getAllByTestId('hardware-node')).toHaveLength(6)
expect(screen.getAllByTestId('workbench-relation-line')).toHaveLength(3)
expect(screen.getByTestId('packaging-wireframe')).toBeInTheDocument()
expect(screen.getAllByTestId('workbench-hud')).toHaveLength(3)
expect(screen.getByTestId('low-noise-model-scene')).toBeInTheDocument()
expect(screen.getByTestId('low-noise-model')).toBeInTheDocument()
expect(screen.getAllByTestId('low-noise-model-board')).toHaveLength(4)
expect(screen.getAllByTestId('low-noise-model-link')).toHaveLength(3)
expect(screen.getAllByTestId('low-noise-model-node')).toHaveLength(5)
expect(screen.getByTestId('low-noise-model-scan')).toBeInTheDocument()
})
it('does not render the previous HUD labels as background content', () => {
render(<AssetWorkbenchEmptyBackdrop />)
expect(screen.queryByText('META COMPONENT')).not.toBeInTheDocument()
expect(screen.queryByText('4D LINK')).not.toBeInTheDocument()
expect(screen.queryByText('ATELIER GRID')).not.toBeInTheDocument()
})
it('allows viewport integrations to adjust the outer layer', () => {