UNIT / D-01 · REV 2.6
- STATUS
- ONLINE
- CLASS
- [VALUE]
- SECTOR
- [VALUE]
- BUILD
- [VALUE]
COMPONENT / spec-card
Product card with a meta header and barcode, a big title, description, action link and spec list on a solid signal offset shadow. Requires @cybr/tokens.
1. Map the @cybr namespace, once per project. Add this inside your components.json, or into its existing "registries" object. No components.json yet? Run npx shadcn@latest init first to create one.
"registries": { "@cybr": "https://cybr-ui.com/r/{name}.json" }2. Add components:
$ npx shadcn@latest add @cybr/spec-cardIf shadcn reports Unknown registry "@cybr", step 1 is missing from that project's components.json.
import type { ComponentProps, ReactNode } from "react";
import { cn } from "@/lib/utils";
export type SpecRow = { label: ReactNode; value: ReactNode; accent?: boolean };
// Decorative barcode bar widths in px, from the artboard.
const BARCODE = ["w-0.5", "w-1", "w-px", "w-0.75", "w-px", "w-1.25", "w-0.5"];
// Product card with a meta header, a big title, a description, an optional
// action and a spec list, sitting on a solid signal offset shadow.
export function SpecCard({
title,
titleAs: Title = "h3",
meta,
specs,
action,
className,
children,
...props
}: Omit<ComponentProps<"article">, "title"> & {
title: ReactNode;
titleAs?: "h2" | "h3" | "h4";
meta?: ReactNode;
specs?: SpecRow[];
action?: ReactNode;
}) {
return (
<article
className={cn(
"mr-2 mb-2 border-(length:--border-base) border-solid border-ink bg-ground-deep font-mono text-ink shadow-[8px_8px_0_0_var(--signal)]",
className,
)}
{...props}
>
<div className="flex items-center justify-between gap-4 border-b-(length:--border-base) border-solid border-ink px-4.5 py-2.5 text-caption tracking-label text-label">
<span>{meta}</span>
<span aria-hidden="true" className="flex shrink-0 gap-0.75">
{BARCODE.map((width, i) => (
<span key={i} className={cn("h-3.5 bg-ink", width)} />
))}
</span>
</div>
<div className="flex flex-col gap-6 p-4.5 sm:flex-row">
<div className="min-w-0 grow">
<Title className="mb-2 font-display text-[2.125rem] [line-height:0.95] uppercase">
{title}
</Title>
{children ? (
<div className="mb-4 text-[0.8125rem] [line-height:1.6] text-label">{children}</div>
) : null}
{action}
</div>
{specs && specs.length > 0 ? (
<dl className="flex shrink-0 flex-col text-caption tracking-[0.1em] sm:w-47.5">
{specs.map((spec, i) => (
<div
key={i}
className="flex justify-between gap-4 border-b-(length:--border-hair) border-solid border-line py-1.75 last:border-b-0"
>
<dt className="text-label">{spec.label}</dt>
<dd className={spec.accent ? "text-signal" : undefined}>{spec.value}</dd>
</div>
))}
</dl>
) : null}
</div>
</article>
);
}
// Ink-filled link with a signal offset shadow that halves on hover.
export function SpecCardAction({ className, ...props }: ComponentProps<"a">) {
return (
<a
className={cn(
"inline-flex min-h-11 items-center border-(length:--border-base) border-solid border-ink bg-ink px-4.5",
"font-mono text-xs font-bold tracking-label text-ground-deep no-underline shadow-[4px_4px_0_0_var(--signal)]",
"transition-[translate,box-shadow] duration-(--duration-snap) ease-step-2 motion-reduce:transition-none",
"hover:translate-0.5 hover:shadow-[2px_2px_0_0_var(--signal)]",
"focus-visible:outline-(length:--focus-width) focus-visible:outline-offset-4 focus-visible:outline-solid focus-visible:outline-signal",
className,
)}
{...props}
/>
);
}
Repo source. The shadcn CLI rewrites import aliases to match your project.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | ReactNode | — | Card title, set in the display face. |
| titleAs | "h2" | "h3" | "h4" | "h3" | Heading level for the title. Match it to the page outline. |
| meta | ReactNode | — | Header text, such as a unit code and revision. |
| specs | { label: ReactNode; value: ReactNode; accent?: boolean }[] | — | Spec list rows. accent colours the value in --signal. The list is left out when empty. |
| action | ReactNode | — | Action under the description, usually a SpecCardAction. |
| children | ReactNode | — | The description: plain text or your own paragraphs. |
| ...props | Omit<ComponentProps<"article">, "title"> | — | Every other native <article> attribute is forwarded; className is merged last with cn(). |
| SpecCardAction ...props | ComponentProps<"a"> | — | A styled native <a>. Pass href and children; className is merged last. |
Needs the stylesheet from @cybr/tokens. After the install, import it in app/globals.css:
@import "tailwindcss";
@import "../styles/cybr-tokens.css";
import { SpecCard, SpecCardAction } from "@/components/ui/spec-card";
export function Module() {
return (
<SpecCard
meta="UNIT / D-01 · REV 2.6"
title="Core module"
action={<SpecCardAction href="/modules/core">VIEW SPEC →</SpecCardAction>}
specs={[{ label: "STATUS", value: "ONLINE", accent: true }]}
>
Hardened compute node for the edge.
</SpecCard>
);
}
--ink border, --ground-deep fill, and the artboard's 8px solid --signal offset shadow. The card keeps 8px of right and bottom margin so a tight parent doesn't clip the shadow.--ink rule.sm, stacked below on small screens.--ink fill with a 4px --signal shadow that halves to 2px as the link moves 2px on hover.--offset-hard token (6px, ink) belongs to the Hard-Offset Button.<article> with a real heading; set its level with titleAs.<dl> with <dt>/<dd> pairs.--signal focus outline. It renders <a>; for client-side routing, wrap the card in your router's link or style your own link with the same classes.--ground-deep: --ink and --label text are above the overview figures for --bg (14.6:1, 7.43:1), and --signal values are above its 4.64:1. The action is --ground-deep on --ink. If you override colours with className, keep text contrast at 4.5:1 or above.