feat(asset-center): add light animated empty workbench model
This commit is contained in:
parent
093d8a215c
commit
22c983ab16
@ -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 => (
|
||||
<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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<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>
|
||||
))}
|
||||
|
||||
{hardwareNodes.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>
|
||||
))}
|
||||
|
||||
<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" />
|
||||
<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={linkClassName}
|
||||
data-testid="low-noise-model-link"
|
||||
className={linkClassName}
|
||||
/>
|
||||
))}
|
||||
{modelBoards.map(boardClassName => (
|
||||
<span
|
||||
key={boardClassName}
|
||||
data-testid="low-noise-model-board"
|
||||
className={boardClassName}
|
||||
/>
|
||||
))}
|
||||
{modelNodes.map(nodeClassName => (
|
||||
<span
|
||||
key={nodeClassName}
|
||||
data-testid="low-noise-model-node"
|
||||
className={nodeClassName}
|
||||
/>
|
||||
))}
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user