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-slab

Examples

Default

Open alone
01 / 02İstanbul
Açık248/300

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.

PropTypeDefaultNotes
media*ReactNode—What plays behind it: a `LazyVideo` or an image covering its box.
name*string—
accentstring—The city's `#rrggbb`: tints the footage, lights the name and paints the call to action.
actionReactNode—The way in, at the foot of the sheet: a `Button` in the city's colour.
classNamestring—
descriptionReactNode—
factsCityFact[]—
indexnumber—Position in the run, for the "01 / 02" counter.
locationReactNode—
statusReactNode—A `ServerStatus`, or any small status line, top right.
tagsstring[]—The city's own tags, as its staff wrote them — kept in their own case.
totalnumber—

Dependencies

npm

  • lucide-react@^0.469.0

Source

components/ds/city-slab.tsxShow
'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 };