Mikkel Manniche
Dansk English Deutsch

Skabelon · React · HTML

Gallery shop

En galleri- og butiksside i Tiles: en sidemenu, en tofarvet overskrift, tællere, arrangementer og kollektioner med valgfri billeder.

Forhåndsvisning

React

Installér skabelonen med shadcn. Den lægges i dit projekt som almindelige komponenter, som du selv retter i.

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/gallery-shop.json
npx shadcn@latest add @manniche/gallery-shop
gallery-shop.tsx
// A gallery shop screen: vertical side menu, a two-tone headline, counters, events and collections.
// Each collection takes an optional image; without one it is drawn as a colour tile, so the demo carries no third-party media.
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { BigNumber } from '@/registry/manniche/chart-kit/chart-kit'
import { DataTile, TileFact } from '@/registry/manniche/data-tile/data-tile'

export type GalleryShopData = {
  menu: { id: string; label: string }[]
  headline: { strong: string; soft: string }
  counters: { id: string; title: string; value: number }[]
  events: { title: string; items: { id: string; date: string; title: string; place: string }[] }
  collections: { title: string; items: { id: string; name: string; works: number; color: string; /** Cover image URL. Falls back to the colour tile. */ image?: string }[] }
}

export type GalleryShopLabels = { note?: string; menuLabel?: string; works?: (n: number) => string }

export type GalleryShopProps = { data: GalleryShopData; labels?: GalleryShopLabels; className?: string }

export function GalleryShop({ data, labels, className }: GalleryShopProps) {
  const note = labels?.note ?? 'Example data.'
  const works = labels?.works ?? ((n: number) => `${n} works`)
  const [current, setCurrent] = useState(data.menu[0]?.id)
  return (
    <div className={cn('@container w-full rounded-2xl bg-background p-3 text-foreground', className)}>
      <div className="flex flex-col gap-3 @lg:flex-row">
        <nav aria-label={labels?.menuLabel ?? 'Gallery'} className="flex shrink-0 gap-1 overflow-x-auto @lg:w-40 @lg:flex-col">
          {data.menu.map((m) => (
            <button
              key={m.id}
              type="button"
              aria-current={m.id === current ? 'page' : undefined}
              onClick={() => setCurrent(m.id)}
              className={cn('shrink-0 rounded-xl px-3 py-2 text-left text-sm text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring', m.id === current && 'bg-foreground text-background hover:bg-foreground hover:text-background')}
            >
              {m.label}
            </button>
          ))}
        </nav>
        <div className="grid min-w-0 flex-1 grid-cols-1 gap-3 @2xl:grid-cols-6">
          <h2 className="px-1 text-[clamp(28px,4vw,48px)] font-semibold leading-tight tracking-tight @2xl:col-span-6">
            {data.headline.strong} <span className="text-muted-foreground">{data.headline.soft}</span>
          </h2>
          {data.counters.map((c) => (
            <div key={c.id} className="@2xl:col-span-2">
              <DataTile className="h-full" title={c.title} density="compact">
                <TileFact>
                  <BigNumber value={c.value} size="lg" />
                </TileFact>
              </DataTile>
            </div>
          ))}
          <div className="@2xl:col-span-3">
            <DataTile className="h-full" title={data.events.title} inverted>
              <ul className="flex flex-col gap-3">
                {data.events.items.map((e) => (
                  <li key={e.id} className="flex flex-col text-sm">
                    <span className="text-xs opacity-70">{e.date} · {e.place}</span>
                    <span className="font-medium">{e.title}</span>
                  </li>
                ))}
              </ul>
            </DataTile>
          </div>
          <div className="@2xl:col-span-3">
            <DataTile className="h-full" title={data.collections.title} footer={note}>
              <ul className="grid grid-cols-2 gap-2">
                {data.collections.items.map((c) => (
                  <li key={c.id} className="flex flex-col gap-1.5">
                    {c.image ? (
                      <img src={c.image} alt="" loading="lazy" className="aspect-[4/3] w-full rounded-xl object-cover" style={{ background: c.color }} />
                    ) : (
                      <span aria-hidden className="aspect-[4/3] rounded-xl" style={{ background: c.color }} />
                    )}
                    <span className="text-sm font-medium">{c.name}</span>
                    <span className="text-xs text-muted-foreground">{works(c.works)}</span>
                  </li>
                ))}
              </ul>
            </DataTile>
          </div>
        </div>
      </div>
    </div>
  )
}
gallery-shop-demo.tsx
import { GalleryShop, type GalleryShopData } from '@/components/gallery-shop'

// Example data only. All names, places and figures are invented.
const data: GalleryShopData = {
  menu: [
    { id: 'home', label: 'Home' },
    { id: 'works', label: 'Works' },
    { id: 'events', label: 'Events' },
    { id: 'shop', label: 'Shop' },
  ],
  headline: { strong: 'Sample gallery.', soft: 'Example art for everyone.' },
  counters: [
    { id: 'a', title: 'Works', value: 240 },
    { id: 'b', title: 'Artists', value: 36 },
    { id: 'c', title: 'Events a year', value: 12 },
  ],
  events: {
    title: 'Events',
    items: [
      { id: 'e1', date: '12 Oct', place: 'Example Hall', title: 'Sample opening night' },
      { id: 'e2', date: '24 Oct', place: 'Demo Room', title: 'Test artist talk' },
    ],
  },
  collections: {
    title: 'Collections',
    items: [
      { id: 'c1', name: 'Sample Blues', works: 18, color: 'oklch(0.7 0.08 250)' },
      { id: 'c2', name: 'Demo Earth', works: 24, color: 'oklch(0.7 0.08 60)' },
      { id: 'c3', name: 'Example Green', works: 9, color: 'oklch(0.7 0.08 150)' },
      { id: 'c4', name: 'Test Rose', works: 14, color: 'oklch(0.7 0.08 10)' },
    ],
  },
}

export default function GalleryShopDemo() {
  return <GalleryShop data={data} />
}

HTML

Hele siden som én fil med al CSS indbygget og intet JavaScript. Knapper, faner og filtre virker med HTML og CSS alene.

Åbn i ny fane

Farven i din egen app

Har du allerede shadcn sat op, giver disse variabler samme farve. Lys i :root, mørk i .dark.

:root {
  --primary: oklch(0.52 0.2 262);
  --primary-foreground: oklch(0.99 0 0);
  --accent: oklch(0.95 0.034 262);
  --accent-foreground: oklch(0.36 0.140 262);
  --ring: oklch(0.52 0.2 262);
}

.dark {
  --primary: oklch(0.74 0.150 262);
  --primary-foreground: oklch(0.18 0.024 262);
  --accent: oklch(0.29 0.050 262);
  --accent-foreground: oklch(0.9 0.050 262);
  --ring: oklch(0.74 0.150 262);
}

Installér

npx shadcn@latest add https://mikkelmanniche.dk/lab/r/gallery-shop.json

Afhængigheder

    Filer

    • gallery-shop.tsx
    • gallery-shop.html

    Manniche Lab · open source

    Gratis værktøjer til AI-agenter og webudvikling.