Brand Rail
Portado desde el riel de etiquetas colgantes de Seda — un desplazamiento continuo, sin pausa, con máscara de degradado en los bordes en vez de un corte seco. El mecanismo es el mismo en las tres variantes; lo que cambia es de qué cuelga (o sobre qué se levanta) cada nombre.
Playground
<BrandRail variant="percha" items={items} />Variantes
variant cambia el objeto y su gesto de suspensión — el desplazamiento, la máscara de bordes y la duplicación del riel para el loop sin costuras son los mismos en las tres.
percha
Cuelga de un hilo de un riel fijo, como una prenda real. El diseño original de Seda, en dorado y sin gris.
sello
Un medallón circular suspendido de una cadena de dos eslabones, con vaivén de altura — más joyería que etiqueta.
vitrina
Invierte la metáfora: cada nombre se levanta desde una línea base sobre su propio pedestal, alturas alternadas.
Código
import * as React from "react";
import { cn } from "./lib/cn";
export interface BrandRailItem {
name: string;
}
export interface BrandRailProps {
items: BrandRailItem[];
/**
* percha — la etiqueta cuelga de un hilo de un riel fijo, como una
* prenda real (el diseño original de Seda, pasado al lenguaje plano
* de Kenza: hilo y aro en dorado en vez de gris, sin el resto de la
* geometría a cambiar).
* sello — un medallón circular suspendido de una cadena de dos
* eslabones, con un leve vaivén de altura por pieza — más joyería
* que etiqueta de ropa.
* vitrina — invierte la metáfora: cada nombre se levanta desde una
* línea base sobre su propio pedestal, como placas en una vitrina de
* museo, con alturas alternadas.
*/
variant?: "percha" | "sello" | "vitrina";
className?: string;
/** segundos que tarda un tramo completo del riel — más alto = más lento */
durationS?: number;
}
const EDGE_FADE = 64;
const FADE_MASK = `linear-gradient(to right, transparent 0, black ${EDGE_FADE}px, black calc(100% - ${EDGE_FADE}px), transparent 100%)`;
const ROTATIONS = [-4, 3, -2, 5, -3, 2];
const THREAD_HEIGHTS = [30, 18, 36, 22, 32, 16];
const CHAIN_OFFSETS = [0, 10, 4, 14, 2, 8];
const PEDESTAL_HEIGHTS = [40, 26, 46, 20, 34, 16];
// el tramo que se repite tiene que ser más ancho que la ventana visible
// o el loop muestra un hueco antes de reiniciar — por eso la lista se
// duplica dos veces (period) y el riel completo son dos períodos
// (loop), la animación mueve solo -50% del riel. Ver
// templates/seda/src/components/brand-tags.tsx, de donde se portó esto.
export function BrandRail({ items, variant = "percha", className, durationS = 52 }: BrandRailProps) {
const period = [...items, ...items];
const loop = [...period, ...period];
return (
<div className={cn("border-y border-border py-10", className)} aria-hidden="true">
{variant !== "vitrina" && <div className="mb-0 h-px w-full bg-border" />}
<div className="overflow-hidden" style={{ WebkitMaskImage: FADE_MASK, maskImage: FADE_MASK }}>
<div
className="flex w-max motion-safe:animate-[brand-rail-scroll_var(--rail-duration)_linear_infinite]"
style={{ "--rail-duration": `${durationS}s` } as React.CSSProperties}
>
{loop.map((item, i) =>
variant === "sello" ? (
<SealTag key={i} name={item.name} i={i} />
) : variant === "vitrina" ? (
<PedestalTag key={i} name={item.name} i={i} />
) : (
<HangingTag key={i} name={item.name} i={i} />
),
)}
</div>
</div>
{variant === "vitrina" && <div className="mt-0 h-px w-full bg-border" />}
{/* @keyframes vía <style> — un solo componente, sin depender de que
el proyecto consumidor registre la animación en su globals.css */}
<style>{`
@keyframes brand-rail-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
`}</style>
</div>
);
}
function HangingTag({ name, i }: { name: string; i: number }) {
return (
<div className="flex flex-col items-center px-6 pb-8 pt-0" style={{ transform: `rotate(${ROTATIONS[i % ROTATIONS.length]}deg)` }}>
<div className="w-px bg-primary/50" style={{ height: THREAD_HEIGHTS[i % THREAD_HEIGHTS.length] }} />
<div className="relative -mt-1 flex h-4 w-4 items-center justify-center rounded-full border border-primary/50 bg-background">
<span className="h-1.5 w-1.5 rounded-full bg-primary/60" />
</div>
<div className="-mt-0.5 rounded-sm border border-border bg-muted px-4 py-2">
<span className="whitespace-nowrap text-sm italic text-foreground/80">{name}</span>
</div>
</div>
);
}
function SealTag({ name, i }: { name: string; i: number }) {
const offset = CHAIN_OFFSETS[i % CHAIN_OFFSETS.length];
return (
<div className="flex flex-col items-center px-7 pb-8 pt-0" style={{ marginTop: offset }}>
<span className="h-2 w-2 rounded-full border border-primary/60" />
<span className="-mt-px h-2 w-2 rounded-full border border-primary/60" />
<div className="-mt-px h-3 w-px bg-primary/50" />
<div className="relative -mt-px flex h-16 w-16 items-center justify-center rounded-full border border-primary/50 bg-muted">
<span aria-hidden="true" className="absolute inset-1 rounded-full border border-border" />
<span className="whitespace-nowrap px-2 text-center text-[0.6875rem] uppercase leading-tight tracking-[0.08em] text-foreground/80">
{name}
</span>
</div>
</div>
);
}
function PedestalTag({ name, i }: { name: string; i: number }) {
return (
<div className="flex flex-col items-center justify-end px-7" style={{ height: 128 }}>
<div className="rounded-sm border border-border bg-muted px-4 py-2">
<span className="whitespace-nowrap text-sm italic text-foreground/80">{name}</span>
</div>
<div className="mt-1 w-px bg-primary/50" style={{ height: PEDESTAL_HEIGHTS[i % PEDESTAL_HEIGHTS.length] }} />
</div>
);
}Código real de packages/ui/src/brand-rail.tsx.
Uso
import { BrandRail } from "./components/ui/brand-rail";
const items = [{ name: "Alba" }, { name: "Fuero" }, { name: "Seda" }, { name: "Vértice" }];
<BrandRail items={items} variant="percha" />API
| Componente | Prop | Tipo | Default |
|---|---|---|---|
| BrandRail | items | BrandRailItem[] | — |
| variant | percha | sello | vitrina | percha | |
| durationS | number | 52 | |
| BrandRailItem | name | string | — |
aria-hidden en el contenedor: el riel es decorativo y se repite varias veces para el loop — la lista real de marcas debe existir también en texto normal en algún otro lugar de la página para lectores de pantalla.