Gallery shell - dark contact sheet
shell-contact-sheet.tsx
shellskill: emil-design-enggallerydarkdense
Variants of gallery-shell: shell-brutalist, shell-editorial
Brief
The prompt that produced this. Re-run it to generate more in the same taste.
- Aesthetic
- dark contact sheet - near-black canvas, full-bleed tiles, edge-to-edge density, chrome that recedes, light only where the work is
- Reference
- https://blingbling.studio/
- Intent
- judging visuals side by side; the designs should be the only lit thing on screen, like negatives on a light table
- Guardrails
- always near-black canvas regardless of app theme, tiles flush to a tight gridnever visible card borders, colored accents competing with the work
Install
Shells are library-internal - they take the registry as props and are not published to the registry.
Source
Meta block and library type import stripped - this is what pastes cleanly elsewhere.
import { Link } from '@tanstack/react-router'
import type React from 'react'
import { useMemo, useState } from 'react'
import { ScaledPreview } from '@/components/preview-frame'
export default function ShellContactSheet({ designs }: ShellProps) {
const [query, setQuery] = useState('')
const [activeTag, setActiveTag] = useState<string | null>(null)
const tags = useMemo(() => [...new Set(designs.flatMap((d) => d.tags))].sort(), [designs])
const visible = useMemo(() => {
const q = query.trim().toLowerCase()
return designs.filter((d) => {
if (activeTag && !d.tags.includes(activeTag)) return false
if (!q) return true
return `${d.title} ${d.id} ${d.skill ?? ''} ${d.tags.join(' ')}`.toLowerCase().includes(q)
})
}, [designs, query, activeTag])
return (
// The canvas is part of this design's identity, so it is pinned dark rather
// than following the app theme.
// 100dvh, not 100vh: on iOS Safari the address bar makes vh jump mid-scroll.
<div className="min-h-[100dvh] bg-[#0a0a0a] text-zinc-100">
<div className="sticky top-0 z-10 border-white/5 border-b bg-[#0a0a0a]/85 backdrop-blur">
<div className="flex flex-wrap items-center gap-4 px-5 py-3">
<span className="font-medium text-[13px] text-zinc-500 tracking-tight">
{visible.length} designs
</span>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Filter…"
className="w-40 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[13px] text-zinc-100 outline-none transition-[border-color] duration-200 ease-[var(--ease-out-strong)] placeholder:text-zinc-600 focus:border-white/25"
/>
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => {
const on = activeTag === tag
return (
<button
key={tag}
type="button"
onClick={() => setActiveTag(on ? null : tag)}
className={`pressable rounded-full px-2.5 py-0.5 text-[11px] transition-[color,background-color,scale] ${
on
? 'bg-zinc-100 text-zinc-900'
: 'bg-white/5 text-zinc-500 hover:bg-white/10 hover:text-zinc-300'
}`}
>
{tag}
</button>
)
})}
</div>
</div>
</div>
{visible.length === 0 ? (
<p className="py-32 text-center text-sm text-zinc-600">Nothing matches.</p>
) : (
<div className="grid grid-cols-1 gap-px bg-white/5 sm:grid-cols-2 xl:grid-cols-3">
{visible.map((design, i) => (
<Link
key={design.id}
to="/d/$id"
params={{ id: design.id }}
className="stagger-in group relative block bg-[#0a0a0a]"
style={{ '--i': Math.min(i, 8) } as React.CSSProperties}
>
<ScaledPreview
id={design.id}
designWidth={1280}
designHeight={900}
className="h-[300px] opacity-90 transition-opacity duration-200 ease-[var(--ease-out-strong)] group-hover:opacity-100"
>
<design.Component designs={designs} />
</ScaledPreview>
{/* Metadata stays out of the way until you go looking for it.
Explicit property list, never `transition-all`. */}
<div className="pointer-events-none absolute inset-x-0 bottom-0 translate-y-1 bg-gradient-to-t from-[#0a0a0a] via-[#0a0a0a]/85 to-transparent px-4 pt-10 pb-3 opacity-0 transition-[opacity,translate] duration-200 ease-[var(--ease-out-strong)] group-hover:translate-y-0 group-hover:opacity-100 motion-reduce:transition-[opacity] motion-reduce:translate-y-0">
<p className="font-medium text-[13px] tracking-tight">{design.title}</p>
{/* One separator per line, max. Columns do the rest. */}
<p className="mt-0.5 flex gap-x-3 text-[11px] text-zinc-500">
<span>{design.id}</span>
{design.skill && <span>{design.skill}</span>}
{design.variantOf && <span className="text-zinc-600">{design.variantOf}</span>}
</p>
</div>
</Link>
))}
</div>
)}
</div>
)
}