Design Library

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 grid
never 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>
  )
}