Pattern
City slab
One city, a full screen: its footage, its colour, its name set enormous.
betav0.1.0 Chromium 103 ui-scale safe
Install
Copies the file into
components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.bunx shadcn@3 add @fastrp/city-slabExamples
Default
01 / 02İstanbul
Ana şehir. Meslekler, çeteler, ekonomi ve polis — hepsi oyuncuların elinde.
- Sesli
- Ciddi RP
- Whitelist
- Giriş
- Başvuru ile
- Para birimi
- $
- Kapasite
- 300 oyuncu
Los Santos
API
<CitySlab />One city, a full screen of its own: its footage tinted in its colour, its name set enormous and bleeding off the bottom, and a glass sheet with what sets it apart. Fills its parent — put it in a `StackedChapter` for the pinned stack.
| Prop | Type | Default | Notes |
|---|---|---|---|
| media* | ReactNode | — | What plays behind it: a `LazyVideo` or an image covering its box. |
| name* | string | — | |
| accent | string | — | The city's `#rrggbb`: tints the footage, lights the name and paints the call to action. |
| action | ReactNode | — | The way in, at the foot of the sheet: a `Button` in the city's colour. |
| className | string | — | |
| description | ReactNode | — | |
| facts | CityFact[] | — | |
| index | number | — | Position in the run, for the "01 / 02" counter. |
| location | ReactNode | — | |
| status | ReactNode | — | A `ServerStatus`, or any small status line, top right. |
| tags | string[] | — | The city's own tags, as its staff wrote them — kept in their own case. |
| total | number | — |
Dependencies
npm
- lucide-react@^0.469.0
Registry
- @fastrp/accent-scope
- @fastrp/fade-in
- @fastrp/utils
Source
components/ds/city-slab.tsxShowHide
'use client';
import { MapPin } from 'lucide-react';
import type * as React from 'react';
import { AccentScope } from '@/components/ds/accent-scope';
import { FadeIn } from '@/components/ds/fade-in';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
export type CityFact = { key: string; label: React.ReactNode; value: React.ReactNode };
export type CitySlabProps = {
name: string;
/** The city's `#rrggbb`: tints the footage, lights the name and paints the call to action. */
accent?: string | null;
/** What plays behind it: a `LazyVideo` or an image covering its box. */
media: React.ReactNode;
/** Position in the run, for the "01 / 02" counter. */
index?: number;
total?: number;
location?: React.ReactNode;
/** A `ServerStatus`, or any small status line, top right. */
status?: React.ReactNode;
description?: React.ReactNode;
/** The city's own tags, as its staff wrote them — kept in their own case. */
tags?: string[];
facts?: CityFact[];
/** The way in, at the foot of the sheet: a `Button` in the city's colour. */
action?: React.ReactNode;
className?: string;
};
const pad = (value: number) => String(value).padStart(2, '0');
/**
* One city, a full screen of its own: its footage tinted in its colour, its name set enormous and
* bleeding off the bottom, and a glass sheet with what sets it apart. Fills its parent — put it in
* a `StackedChapter` for the pinned stack.
*/
function CitySlab({
name,
accent,
media,
index,
total,
location,
status,
description,
tags,
facts,
action,
className,
}: CitySlabProps) {
return (
<AccentScope
accent={accent}
data-slot="city-slab"
className={cn('relative h-full min-h-[inherit] overflow-hidden bg-ink', className)}
>
<div className="absolute inset-0 [&>*]:size-full">{media}</div>
<div className="pointer-events-none absolute inset-0 bg-black/55" />
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_at_15%_110%,hsl(var(--primary)/0.4),transparent_55%),linear-gradient(to_top,#000_0%,transparent_45%)]" />
<div className="relative flex h-full min-h-[inherit] flex-col gap-10 px-6 pb-10 pt-24 md:px-12 md:pb-12 md:pt-28 lg:px-20">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-4 text-xs uppercase tracking-widest text-foreground/60">
{index !== undefined && total !== undefined && (
<span className="tabular-nums">
{pad(index + 1)} / {pad(total)}
</span>
)}
{location && (
<span className="flex items-center gap-1.5">
<MapPin className="size-3.5" />
{location}
</span>
)}
</div>
{status}
</div>
{/* The sheet sits centred in the space left, clear of both edges whatever the height. */}
<div className="relative z-raised flex flex-1 items-center justify-end">
<FadeIn from={{ x: 32 }} className="glass w-full max-w-md p-6 md:p-8">
{description && (
<p className="text-sm leading-relaxed text-foreground/80">{description}</p>
)}
{tags && tags.length > 0 && (
<ul className="mt-5 flex flex-wrap gap-2">
{tags.map((tag) => (
<li
key={tag}
className="border border-foreground/15 bg-foreground/[0.04] px-3 py-1 text-xs text-foreground/75"
>
{tag}
</li>
))}
</ul>
)}
{facts && facts.length > 0 && (
<dl className="mt-6 divide-y divide-foreground/10 border-y border-foreground/10">
{facts.map((fact) => (
<div key={fact.key} className="flex items-center justify-between gap-6 py-2.5">
<dt className="text-xs uppercase tracking-widest text-foreground/40">
{fact.label}
</dt>
<dd className="text-right text-sm text-foreground">{fact.value}</dd>
</div>
))}
</dl>
)}
{action && <div className="mt-6 [&>*]:w-full">{action}</div>}
</FadeIn>
</div>
{/* Behind the sheet on wide screens, bleeding off the bottom edge; under it on phones. */}
<h3 className="pointer-events-none select-none font-display text-[22vw] leading-[0.8] text-foreground [text-shadow:0_0_6rem_hsl(var(--primary)/0.33)] md:absolute md:bottom-0 md:left-12 md:-mb-[0.1em] md:text-[15vw] lg:left-20">
{name}
</h3>
</div>
</AccentScope>
);
}
export { CitySlab };