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.jsonnpx shadcn@latest add @manniche/gallery-shopgallery-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.
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