Install Brick
in 30 seconds.
Pick your AI. Hit copy. Paste it into a fresh chat in your project. Watch the panels arrive embossed.
Install the Wardrobe Brick theme in my project. I'm using Claude Code / Cursor. Apply every step below as written.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap");
/* =========================================================
Wardrobe — BRICK tokens
Hardware/skeumorphic. Embossed panels, raised buttons,
multi-layer shadows. Activate via data-system="brick".
========================================================= */
:where([data-system="brick"]) {
/* ---- Surface ---- */
--color-bg: #C8C9C4; /* outside the device */
--color-casing: #E3E4DF; /* device panel surface */
--color-casing-tint: #DADBD4; /* slightly darker casing for inputs / inset wells */
--color-casing-button: #E8E8E4; /* secondary button surface */
--color-terminal: #DCDDDA; /* inset display screens */
--color-recess: rgba(0, 0, 0, 0.03);
--color-fg: #1A1A1A;
--color-fg-muted: #555555;
--color-fg-label: #6A6B66;
--color-hole: #1C1C1A; /* dark accents / punched holes */
/* ---- Primary palette (LEGO-style functional colors) ---- */
--color-red: #D03027;
--color-red-shade: #9B1C15;
--color-blue: #0055A4;
--color-blue-shade: #00366D;
--color-yellow: #F2A900;
--color-yellow-shade: #B88000;
--color-green: #00853E;
--color-green-shade: #005B2A;
--color-white: #F4F4F2;
--color-white-shade: #BDBDBD;
/* ---- Status (badges) ---- */
--color-status-ok: var(--color-green);
--color-status-proc: var(--color-blue);
--color-status-wait: var(--color-yellow);
--color-status-archive: var(--color-fg-muted);
--color-status-bg: #D1D2CD;
/* ---- Panel gaps + highlights (kritisk for embossed effekt) ---- */
--highlight: rgba(255, 255, 255, 0.7);
--panel-gap-dark: rgba(0, 0, 0, 0.15);
--panel-gap-light: rgba(255, 255, 255, 0.8);
/* ---- Multi-layer shadows (signaturen) ---- */
--shadow-device: 0 20px 50px rgba(0, 0, 0, 0.15),
0 5px 15px rgba(0, 0, 0, 0.05),
inset 1px 1px 2px var(--highlight),
inset -1px -1px 3px rgba(0, 0, 0, 0.1);
--shadow-button-raised: 0 4px 8px rgba(0, 0, 0, 0.2);
--shadow-button-secondary: inset 1px 1px 1px rgba(255, 255, 255, 0.5);
--shadow-button-pressed: inset 1px 1px 2px rgba(0, 0, 0, 0.1);
--shadow-inset-deep: inset 2px 2px 5px rgba(0, 0, 0, 0.05);
--shadow-inset-input: inset 1px 1px 2px rgba(0, 0, 0, 0.1);
--shadow-tile: 4px 4px 10px rgba(0, 0, 0, 0.08),
inset 2px 2px 4px rgba(0, 0, 0, 0.05),
inset -2px -2px 4px rgba(255, 255, 255, 0.5);
--shadow-stud: inset 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-swatch: inset 0 4px 8px rgba(255, 255, 255, 0.3),
inset 0 -4px 8px rgba(0, 0, 0, 0.2),
0 4px 10px rgba(0, 0, 0, 0.15);
/* ---- Radius ---- */
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-device: 12px;
--radius-full: 9999px;
/* ---- Typography ---- */
--font-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--text-micro: 10px;
--text-xs: 11px;
--text-sm: 12px;
--text-base: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 24px;
--text-3xl: 32px;
--label-letter-spacing: 0.1em;
--brand-letter-spacing: -0.02em;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* ---- Spacing ---- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* ---- Motion ---- */
--duration-fast: 100ms;
--duration-base: 200ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="brick"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("brk-btn", `brk-btn--${variant}`, `brk-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
numericBadge?: string;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
numericBadge,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
{numericBadge && (
<span
className="brk-field__label"
style={{ position: "absolute", top: 0, right: 0 }}
>
{numericBadge}
</span>
)}
<input
id={inputId}
className={cn(
"brk-input",
`brk-input--${size}`,
variant === "error" && "brk-input--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"brk-field__hint",
variant === "error" && "brk-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
export function Textarea({
size = "md",
variant = "default",
label,
hint,
className,
id,
...props
}: TextareaProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"brk-textarea",
`brk-input--${size}`,
variant === "error" && "brk-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"brk-field__hint",
variant === "error" && "brk-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SelectProps = {
options: Array<{ value: string; label: string }>;
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
label?: string;
} & Omit<
React.SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "size"
>;
export function Select({
options,
value,
onValueChange,
placeholder,
label,
className,
id,
...props
}: SelectProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
<div className="brk-select-wrap">
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("brk-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</label>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "recessed" | "device";
modelNumber?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
modelNumber,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"brk-card",
variant !== "default" && `brk-card--${variant}`,
className,
)}
{...props}
>
{modelNumber && (
<span className="brk-card__model">{modelNumber}</span>
)}
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "filled" | "ok" | "proc" | "wait" | "archive";
children: React.ReactNode;
className?: string;
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"brk-badge",
variant !== "default" && `brk-badge--${variant}`,
className,
)}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
modelNumber?: string;
children: React.ReactNode;
};
export function Dialog({
open,
onOpenChange,
title,
modelNumber,
children,
}: DialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const previousFocus = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
previousFocus.current = document.activeElement as HTMLElement;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
return;
}
if (e.key !== "Tab" || !focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
previousFocus.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="brk-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="brk-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="brk-dialog-title"
>
<button
type="button"
className="brk-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<div className="brk-dialog__head">
<h2 id="brk-dialog-title" className="brk-dialog__title">
{title}
</h2>
{modelNumber && (
<span className="brk-dialog__model">{modelNumber}</span>
)}
</div>
{children}
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
className?: string;
};
export function Tabs({ tabs, activeId, onTabChange, className }: TabsProps) {
return (
<div role="tablist" className={cn("brk-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"brk-tabs__btn",
tab.id === activeId && "brk-tabs__btn--active",
)}
onClick={() => onTabChange(tab.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const idx = tabs.findIndex((t) => t.id === activeId);
const next =
e.key === "ArrowRight"
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length;
onTabChange(tabs[next].id);
}}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
};
export function Switch({ checked, onCheckedChange, label, id }: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-switch" htmlFor={inputId}>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="brk-switch__input"
/>
<span className="brk-switch__track">
<span className="brk-switch__thumb" />
</span>
{label && <span>{label}</span>}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
export type ToastVariant = "default" | "success" | "error";
export type ToastProps = {
title: string;
description?: string;
variant?: ToastVariant;
timestamp?: string;
};
type ToastEntry = ToastProps & { id: number };
type ToastContextValue = {
show: (toast: ToastProps) => void;
};
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
return ctx;
}
function defaultTime() {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
const idRef = React.useRef(0);
const show = React.useCallback((toast: ToastProps) => {
const id = ++idRef.current;
setToasts((prev) => [
...prev,
{ timestamp: defaultTime(), ...toast, id },
]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="brk-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
export function Toast({
title,
description,
variant = "default",
timestamp,
}: ToastProps) {
return (
<div
className={cn(
"brk-toast",
variant === "success" && "brk-toast--success",
variant === "error" && "brk-toast--error",
)}
role="status"
>
<span className="brk-toast__title">[ {title} ]</span>
{description && (
<span className="brk-toast__desc">{description}</span>
)}
{timestamp && <span className="brk-toast__time">{timestamp}</span>}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — BRICK globals
Loads Inter + JBM, then component styles.
Only active under data-system="brick".
========================================================= */
[data-system="brick"] *,
[data-system="brick"] *::before,
[data-system="brick"] *::after {
box-sizing: border-box;
}
[data-system="brick"] {
min-height: 100vh;
}
/* ===========================
BUTTON
=========================== */
.brk-btn {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: var(--radius-md);
line-height: 1;
text-decoration: none;
transition: transform var(--duration-fast) var(--easing),
box-shadow var(--duration-fast) var(--easing),
background var(--duration-base) var(--easing);
}
.brk-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.brk-btn--sm { height: 32px; padding: 0 14px; font-size: 11px; }
.brk-btn--md { height: 40px; padding: 0 18px; font-size: 12px; }
.brk-btn--lg { height: 48px; padding: 0 24px; font-size: 13px; }
.brk-btn--primary {
background: var(--color-fg);
color: #FFFFFF;
box-shadow: var(--shadow-button-raised);
}
.brk-btn--primary:not(:disabled):hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
}
.brk-btn--primary:not(:disabled):active {
transform: translateY(1px);
box-shadow: var(--shadow-button-pressed);
}
.brk-btn--secondary {
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
box-shadow: var(--shadow-button-secondary);
}
.brk-btn--secondary:not(:disabled):hover {
background: #EEEFEA;
}
.brk-btn--secondary:not(:disabled):active {
background: #DFDFDB;
box-shadow: var(--shadow-button-pressed);
transform: translateY(1px);
}
.brk-btn--destructive {
background: var(--color-red);
color: #FFFFFF;
box-shadow: var(--shadow-button-raised);
}
.brk-btn--destructive:not(:disabled):active {
transform: translateY(1px);
background: var(--color-red-shade);
}
.brk-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ===========================
FIELD (Input / Textarea / Select wrapper)
=========================== */
.brk-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
position: relative;
}
.brk-field__label {
font-family: var(--font-mono);
font-size: var(--text-micro);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-label);
line-height: 1;
}
.brk-field__hint {
font-family: var(--font-mono);
font-size: var(--text-micro);
color: var(--color-fg-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.brk-field__hint--error { color: var(--color-red); }
/* ===========================
INPUT / TEXTAREA
=========================== */
.brk-input,
.brk-textarea {
font-family: var(--font-mono);
font-size: var(--text-base);
color: var(--color-fg);
background: var(--color-casing-tint);
border: none;
border-bottom: 1px solid transparent;
border-radius: var(--radius-sm);
padding: 8px 12px;
width: 100%;
outline: none;
box-shadow: var(--shadow-inset-input);
transition: border-color var(--duration-fast) var(--easing);
}
.brk-input:focus,
.brk-textarea:focus {
border-bottom-color: var(--color-fg);
}
.brk-input::placeholder,
.brk-textarea::placeholder {
color: var(--color-fg-label);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
}
.brk-input--error,
.brk-textarea--error { border-bottom-color: var(--color-red); }
.brk-input--sm { padding: 6px 10px; font-size: 12px; }
.brk-input--md { padding: 8px 12px; font-size: 13px; }
.brk-input--lg { padding: 12px 14px; font-size: 14px; }
.brk-textarea {
min-height: 100px;
resize: vertical;
}
/* ===========================
SELECT (custom)
=========================== */
.brk-select-wrap {
position: relative;
width: 100%;
}
.brk-select {
font-family: var(--font-mono);
font-size: var(--text-base);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-fg);
background: var(--color-casing-tint);
border: none;
border-radius: var(--radius-sm);
padding: 8px 32px 8px 12px;
width: 100%;
outline: none;
box-shadow: var(--shadow-inset-input);
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%231A1A1A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 10px center;
}
/* ===========================
CARD
=========================== */
.brk-card {
background: var(--color-casing);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: var(--radius-lg);
padding: var(--space-6);
box-shadow: var(--shadow-tile);
position: relative;
}
.brk-card--recessed {
background: var(--color-recess);
border: 1px solid var(--panel-gap-dark);
box-shadow: 0 1px 0 var(--panel-gap-light);
}
.brk-card--device {
background: var(--color-casing);
box-shadow: var(--shadow-device);
border: none;
border-radius: var(--radius-device);
padding: var(--space-8);
}
.brk-card__model {
position: absolute;
top: 14px;
right: 18px;
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
color: var(--color-fg-muted);
letter-spacing: var(--label-letter-spacing);
text-transform: uppercase;
}
/* ===========================
BADGE (status code)
=========================== */
.brk-badge {
display: inline-flex;
align-items: center;
background: var(--color-status-bg);
color: var(--color-fg);
border-radius: var(--radius-xs);
padding: 2px 6px;
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1.4;
}
.brk-badge--ok { color: var(--color-status-ok); }
.brk-badge--proc { color: var(--color-status-proc); }
.brk-badge--wait { color: var(--color-status-wait); }
.brk-badge--archive { color: var(--color-status-archive); }
.brk-badge--filled {
background: var(--color-fg);
color: #FFFFFF;
}
/* ===========================
DIALOG
=========================== */
.brk-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: brk-fade-in 200ms var(--easing);
}
.brk-dialog {
background: var(--color-casing);
border-radius: var(--radius-device);
box-shadow: var(--shadow-device);
max-width: 520px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: brk-pop-in 220ms var(--easing);
}
.brk-dialog__head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin: 0 0 var(--space-5);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--panel-gap-dark);
box-shadow: 0 1px 0 var(--panel-gap-light);
}
.brk-dialog__title {
font-family: var(--font-display);
font-weight: var(--weight-semibold);
font-size: var(--text-xl);
letter-spacing: var(--brand-letter-spacing);
margin: 0;
}
.brk-dialog__model {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
color: var(--color-fg-muted);
letter-spacing: var(--label-letter-spacing);
text-transform: uppercase;
}
.brk-dialog__close {
position: absolute;
top: 14px;
right: 14px;
width: 28px;
height: 28px;
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--font-mono);
font-size: 14px;
line-height: 1;
}
.brk-dialog__close:hover { background: #EEEFEA; }
.brk-dialog__close:active { box-shadow: var(--shadow-button-pressed); }
@keyframes brk-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes brk-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ===========================
TABS
=========================== */
.brk-tabs {
display: inline-flex;
gap: 4px;
flex-wrap: wrap;
}
.brk-tabs__btn {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
padding: 8px 14px;
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-button-secondary);
cursor: pointer;
line-height: 1;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
}
.brk-tabs__btn:hover { background: #EEEFEA; }
.brk-tabs__btn--active {
background: var(--color-fg);
color: #FFFFFF;
border-color: var(--color-fg);
box-shadow: var(--shadow-button-raised);
}
.brk-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ===========================
SWITCH (physical hardware switch)
=========================== */
.brk-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
}
.brk-switch__track {
width: 44px;
height: 24px;
background: #D1D2CD;
border-radius: var(--radius-full);
position: relative;
box-shadow: var(--shadow-inset-input);
transition: background var(--duration-base) var(--easing);
flex-shrink: 0;
}
.brk-switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: var(--color-white);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: transform var(--duration-base) var(--easing);
}
.brk-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.brk-switch__input:checked + .brk-switch__track {
background: var(--color-green);
}
.brk-switch__input:checked + .brk-switch__track .brk-switch__thumb {
transform: translateX(20px);
}
.brk-switch__input:focus-visible + .brk-switch__track {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ===========================
TOAST (status display)
=========================== */
.brk-toast-region {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.brk-toast {
background: var(--color-white);
color: var(--color-fg);
border-radius: var(--radius-md);
padding: 14px 18px;
border-left: 3px solid var(--color-fg-muted);
box-shadow: var(--shadow-tile);
font-family: var(--font-body);
font-size: var(--text-base);
pointer-events: auto;
animation: brk-toast-in 280ms var(--easing);
display: flex;
flex-direction: column;
gap: 4px;
min-width: 240px;
max-width: 360px;
position: relative;
}
.brk-toast__title {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: 11px;
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
}
.brk-toast__desc {
font-family: var(--font-body);
font-size: 12px;
color: var(--color-fg-muted);
line-height: 1.4;
}
.brk-toast__time {
position: absolute;
top: 10px;
right: 14px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-fg-label);
letter-spacing: 0.05em;
}
.brk-toast--success { border-left-color: var(--color-green); }
.brk-toast--success .brk-toast__title { color: var(--color-green); }
.brk-toast--error { border-left-color: var(--color-red); }
.brk-toast--error .brk-toast__title { color: var(--color-red); }
@keyframes brk-toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===========================
STUDS / SWATCHES (utility)
=========================== */
.brk-stud {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: var(--shadow-stud);
vertical-align: middle;
}
.brk-stud--lg { width: 18px; height: 18px; }
Step 5 — Wrap the section/page you want themed with <div data-system="brick">:
<div data-system="brick">
<Button>HELLO BRICK</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Brick.
Install the Wardrobe Brick theme in this v0 project. Treat each step below as a concrete file or edit you must make.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap");
/* =========================================================
Wardrobe — BRICK tokens
Hardware/skeumorphic. Embossed panels, raised buttons,
multi-layer shadows. Activate via data-system="brick".
========================================================= */
:where([data-system="brick"]) {
/* ---- Surface ---- */
--color-bg: #C8C9C4; /* outside the device */
--color-casing: #E3E4DF; /* device panel surface */
--color-casing-tint: #DADBD4; /* slightly darker casing for inputs / inset wells */
--color-casing-button: #E8E8E4; /* secondary button surface */
--color-terminal: #DCDDDA; /* inset display screens */
--color-recess: rgba(0, 0, 0, 0.03);
--color-fg: #1A1A1A;
--color-fg-muted: #555555;
--color-fg-label: #6A6B66;
--color-hole: #1C1C1A; /* dark accents / punched holes */
/* ---- Primary palette (LEGO-style functional colors) ---- */
--color-red: #D03027;
--color-red-shade: #9B1C15;
--color-blue: #0055A4;
--color-blue-shade: #00366D;
--color-yellow: #F2A900;
--color-yellow-shade: #B88000;
--color-green: #00853E;
--color-green-shade: #005B2A;
--color-white: #F4F4F2;
--color-white-shade: #BDBDBD;
/* ---- Status (badges) ---- */
--color-status-ok: var(--color-green);
--color-status-proc: var(--color-blue);
--color-status-wait: var(--color-yellow);
--color-status-archive: var(--color-fg-muted);
--color-status-bg: #D1D2CD;
/* ---- Panel gaps + highlights (kritisk for embossed effekt) ---- */
--highlight: rgba(255, 255, 255, 0.7);
--panel-gap-dark: rgba(0, 0, 0, 0.15);
--panel-gap-light: rgba(255, 255, 255, 0.8);
/* ---- Multi-layer shadows (signaturen) ---- */
--shadow-device: 0 20px 50px rgba(0, 0, 0, 0.15),
0 5px 15px rgba(0, 0, 0, 0.05),
inset 1px 1px 2px var(--highlight),
inset -1px -1px 3px rgba(0, 0, 0, 0.1);
--shadow-button-raised: 0 4px 8px rgba(0, 0, 0, 0.2);
--shadow-button-secondary: inset 1px 1px 1px rgba(255, 255, 255, 0.5);
--shadow-button-pressed: inset 1px 1px 2px rgba(0, 0, 0, 0.1);
--shadow-inset-deep: inset 2px 2px 5px rgba(0, 0, 0, 0.05);
--shadow-inset-input: inset 1px 1px 2px rgba(0, 0, 0, 0.1);
--shadow-tile: 4px 4px 10px rgba(0, 0, 0, 0.08),
inset 2px 2px 4px rgba(0, 0, 0, 0.05),
inset -2px -2px 4px rgba(255, 255, 255, 0.5);
--shadow-stud: inset 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-swatch: inset 0 4px 8px rgba(255, 255, 255, 0.3),
inset 0 -4px 8px rgba(0, 0, 0, 0.2),
0 4px 10px rgba(0, 0, 0, 0.15);
/* ---- Radius ---- */
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-device: 12px;
--radius-full: 9999px;
/* ---- Typography ---- */
--font-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--text-micro: 10px;
--text-xs: 11px;
--text-sm: 12px;
--text-base: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 24px;
--text-3xl: 32px;
--label-letter-spacing: 0.1em;
--brand-letter-spacing: -0.02em;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* ---- Spacing ---- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* ---- Motion ---- */
--duration-fast: 100ms;
--duration-base: 200ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="brick"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("brk-btn", `brk-btn--${variant}`, `brk-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
numericBadge?: string;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
numericBadge,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
{numericBadge && (
<span
className="brk-field__label"
style={{ position: "absolute", top: 0, right: 0 }}
>
{numericBadge}
</span>
)}
<input
id={inputId}
className={cn(
"brk-input",
`brk-input--${size}`,
variant === "error" && "brk-input--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"brk-field__hint",
variant === "error" && "brk-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
export function Textarea({
size = "md",
variant = "default",
label,
hint,
className,
id,
...props
}: TextareaProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"brk-textarea",
`brk-input--${size}`,
variant === "error" && "brk-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"brk-field__hint",
variant === "error" && "brk-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SelectProps = {
options: Array<{ value: string; label: string }>;
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
label?: string;
} & Omit<
React.SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "size"
>;
export function Select({
options,
value,
onValueChange,
placeholder,
label,
className,
id,
...props
}: SelectProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
<div className="brk-select-wrap">
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("brk-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</label>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "recessed" | "device";
modelNumber?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
modelNumber,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"brk-card",
variant !== "default" && `brk-card--${variant}`,
className,
)}
{...props}
>
{modelNumber && (
<span className="brk-card__model">{modelNumber}</span>
)}
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "filled" | "ok" | "proc" | "wait" | "archive";
children: React.ReactNode;
className?: string;
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"brk-badge",
variant !== "default" && `brk-badge--${variant}`,
className,
)}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
modelNumber?: string;
children: React.ReactNode;
};
export function Dialog({
open,
onOpenChange,
title,
modelNumber,
children,
}: DialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const previousFocus = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
previousFocus.current = document.activeElement as HTMLElement;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
return;
}
if (e.key !== "Tab" || !focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
previousFocus.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="brk-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="brk-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="brk-dialog-title"
>
<button
type="button"
className="brk-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<div className="brk-dialog__head">
<h2 id="brk-dialog-title" className="brk-dialog__title">
{title}
</h2>
{modelNumber && (
<span className="brk-dialog__model">{modelNumber}</span>
)}
</div>
{children}
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
className?: string;
};
export function Tabs({ tabs, activeId, onTabChange, className }: TabsProps) {
return (
<div role="tablist" className={cn("brk-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"brk-tabs__btn",
tab.id === activeId && "brk-tabs__btn--active",
)}
onClick={() => onTabChange(tab.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const idx = tabs.findIndex((t) => t.id === activeId);
const next =
e.key === "ArrowRight"
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length;
onTabChange(tabs[next].id);
}}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
};
export function Switch({ checked, onCheckedChange, label, id }: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-switch" htmlFor={inputId}>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="brk-switch__input"
/>
<span className="brk-switch__track">
<span className="brk-switch__thumb" />
</span>
{label && <span>{label}</span>}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
export type ToastVariant = "default" | "success" | "error";
export type ToastProps = {
title: string;
description?: string;
variant?: ToastVariant;
timestamp?: string;
};
type ToastEntry = ToastProps & { id: number };
type ToastContextValue = {
show: (toast: ToastProps) => void;
};
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
return ctx;
}
function defaultTime() {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
const idRef = React.useRef(0);
const show = React.useCallback((toast: ToastProps) => {
const id = ++idRef.current;
setToasts((prev) => [
...prev,
{ timestamp: defaultTime(), ...toast, id },
]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="brk-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
export function Toast({
title,
description,
variant = "default",
timestamp,
}: ToastProps) {
return (
<div
className={cn(
"brk-toast",
variant === "success" && "brk-toast--success",
variant === "error" && "brk-toast--error",
)}
role="status"
>
<span className="brk-toast__title">[ {title} ]</span>
{description && (
<span className="brk-toast__desc">{description}</span>
)}
{timestamp && <span className="brk-toast__time">{timestamp}</span>}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — BRICK globals
Loads Inter + JBM, then component styles.
Only active under data-system="brick".
========================================================= */
[data-system="brick"] *,
[data-system="brick"] *::before,
[data-system="brick"] *::after {
box-sizing: border-box;
}
[data-system="brick"] {
min-height: 100vh;
}
/* ===========================
BUTTON
=========================== */
.brk-btn {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: var(--radius-md);
line-height: 1;
text-decoration: none;
transition: transform var(--duration-fast) var(--easing),
box-shadow var(--duration-fast) var(--easing),
background var(--duration-base) var(--easing);
}
.brk-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.brk-btn--sm { height: 32px; padding: 0 14px; font-size: 11px; }
.brk-btn--md { height: 40px; padding: 0 18px; font-size: 12px; }
.brk-btn--lg { height: 48px; padding: 0 24px; font-size: 13px; }
.brk-btn--primary {
background: var(--color-fg);
color: #FFFFFF;
box-shadow: var(--shadow-button-raised);
}
.brk-btn--primary:not(:disabled):hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
}
.brk-btn--primary:not(:disabled):active {
transform: translateY(1px);
box-shadow: var(--shadow-button-pressed);
}
.brk-btn--secondary {
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
box-shadow: var(--shadow-button-secondary);
}
.brk-btn--secondary:not(:disabled):hover {
background: #EEEFEA;
}
.brk-btn--secondary:not(:disabled):active {
background: #DFDFDB;
box-shadow: var(--shadow-button-pressed);
transform: translateY(1px);
}
.brk-btn--destructive {
background: var(--color-red);
color: #FFFFFF;
box-shadow: var(--shadow-button-raised);
}
.brk-btn--destructive:not(:disabled):active {
transform: translateY(1px);
background: var(--color-red-shade);
}
.brk-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ===========================
FIELD (Input / Textarea / Select wrapper)
=========================== */
.brk-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
position: relative;
}
.brk-field__label {
font-family: var(--font-mono);
font-size: var(--text-micro);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-label);
line-height: 1;
}
.brk-field__hint {
font-family: var(--font-mono);
font-size: var(--text-micro);
color: var(--color-fg-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.brk-field__hint--error { color: var(--color-red); }
/* ===========================
INPUT / TEXTAREA
=========================== */
.brk-input,
.brk-textarea {
font-family: var(--font-mono);
font-size: var(--text-base);
color: var(--color-fg);
background: var(--color-casing-tint);
border: none;
border-bottom: 1px solid transparent;
border-radius: var(--radius-sm);
padding: 8px 12px;
width: 100%;
outline: none;
box-shadow: var(--shadow-inset-input);
transition: border-color var(--duration-fast) var(--easing);
}
.brk-input:focus,
.brk-textarea:focus {
border-bottom-color: var(--color-fg);
}
.brk-input::placeholder,
.brk-textarea::placeholder {
color: var(--color-fg-label);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
}
.brk-input--error,
.brk-textarea--error { border-bottom-color: var(--color-red); }
.brk-input--sm { padding: 6px 10px; font-size: 12px; }
.brk-input--md { padding: 8px 12px; font-size: 13px; }
.brk-input--lg { padding: 12px 14px; font-size: 14px; }
.brk-textarea {
min-height: 100px;
resize: vertical;
}
/* ===========================
SELECT (custom)
=========================== */
.brk-select-wrap {
position: relative;
width: 100%;
}
.brk-select {
font-family: var(--font-mono);
font-size: var(--text-base);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-fg);
background: var(--color-casing-tint);
border: none;
border-radius: var(--radius-sm);
padding: 8px 32px 8px 12px;
width: 100%;
outline: none;
box-shadow: var(--shadow-inset-input);
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%231A1A1A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 10px center;
}
/* ===========================
CARD
=========================== */
.brk-card {
background: var(--color-casing);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: var(--radius-lg);
padding: var(--space-6);
box-shadow: var(--shadow-tile);
position: relative;
}
.brk-card--recessed {
background: var(--color-recess);
border: 1px solid var(--panel-gap-dark);
box-shadow: 0 1px 0 var(--panel-gap-light);
}
.brk-card--device {
background: var(--color-casing);
box-shadow: var(--shadow-device);
border: none;
border-radius: var(--radius-device);
padding: var(--space-8);
}
.brk-card__model {
position: absolute;
top: 14px;
right: 18px;
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
color: var(--color-fg-muted);
letter-spacing: var(--label-letter-spacing);
text-transform: uppercase;
}
/* ===========================
BADGE (status code)
=========================== */
.brk-badge {
display: inline-flex;
align-items: center;
background: var(--color-status-bg);
color: var(--color-fg);
border-radius: var(--radius-xs);
padding: 2px 6px;
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1.4;
}
.brk-badge--ok { color: var(--color-status-ok); }
.brk-badge--proc { color: var(--color-status-proc); }
.brk-badge--wait { color: var(--color-status-wait); }
.brk-badge--archive { color: var(--color-status-archive); }
.brk-badge--filled {
background: var(--color-fg);
color: #FFFFFF;
}
/* ===========================
DIALOG
=========================== */
.brk-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: brk-fade-in 200ms var(--easing);
}
.brk-dialog {
background: var(--color-casing);
border-radius: var(--radius-device);
box-shadow: var(--shadow-device);
max-width: 520px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: brk-pop-in 220ms var(--easing);
}
.brk-dialog__head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin: 0 0 var(--space-5);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--panel-gap-dark);
box-shadow: 0 1px 0 var(--panel-gap-light);
}
.brk-dialog__title {
font-family: var(--font-display);
font-weight: var(--weight-semibold);
font-size: var(--text-xl);
letter-spacing: var(--brand-letter-spacing);
margin: 0;
}
.brk-dialog__model {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
color: var(--color-fg-muted);
letter-spacing: var(--label-letter-spacing);
text-transform: uppercase;
}
.brk-dialog__close {
position: absolute;
top: 14px;
right: 14px;
width: 28px;
height: 28px;
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--font-mono);
font-size: 14px;
line-height: 1;
}
.brk-dialog__close:hover { background: #EEEFEA; }
.brk-dialog__close:active { box-shadow: var(--shadow-button-pressed); }
@keyframes brk-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes brk-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ===========================
TABS
=========================== */
.brk-tabs {
display: inline-flex;
gap: 4px;
flex-wrap: wrap;
}
.brk-tabs__btn {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
padding: 8px 14px;
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-button-secondary);
cursor: pointer;
line-height: 1;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
}
.brk-tabs__btn:hover { background: #EEEFEA; }
.brk-tabs__btn--active {
background: var(--color-fg);
color: #FFFFFF;
border-color: var(--color-fg);
box-shadow: var(--shadow-button-raised);
}
.brk-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ===========================
SWITCH (physical hardware switch)
=========================== */
.brk-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
}
.brk-switch__track {
width: 44px;
height: 24px;
background: #D1D2CD;
border-radius: var(--radius-full);
position: relative;
box-shadow: var(--shadow-inset-input);
transition: background var(--duration-base) var(--easing);
flex-shrink: 0;
}
.brk-switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: var(--color-white);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: transform var(--duration-base) var(--easing);
}
.brk-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.brk-switch__input:checked + .brk-switch__track {
background: var(--color-green);
}
.brk-switch__input:checked + .brk-switch__track .brk-switch__thumb {
transform: translateX(20px);
}
.brk-switch__input:focus-visible + .brk-switch__track {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ===========================
TOAST (status display)
=========================== */
.brk-toast-region {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.brk-toast {
background: var(--color-white);
color: var(--color-fg);
border-radius: var(--radius-md);
padding: 14px 18px;
border-left: 3px solid var(--color-fg-muted);
box-shadow: var(--shadow-tile);
font-family: var(--font-body);
font-size: var(--text-base);
pointer-events: auto;
animation: brk-toast-in 280ms var(--easing);
display: flex;
flex-direction: column;
gap: 4px;
min-width: 240px;
max-width: 360px;
position: relative;
}
.brk-toast__title {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: 11px;
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
}
.brk-toast__desc {
font-family: var(--font-body);
font-size: 12px;
color: var(--color-fg-muted);
line-height: 1.4;
}
.brk-toast__time {
position: absolute;
top: 10px;
right: 14px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-fg-label);
letter-spacing: 0.05em;
}
.brk-toast--success { border-left-color: var(--color-green); }
.brk-toast--success .brk-toast__title { color: var(--color-green); }
.brk-toast--error { border-left-color: var(--color-red); }
.brk-toast--error .brk-toast__title { color: var(--color-red); }
@keyframes brk-toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===========================
STUDS / SWATCHES (utility)
=========================== */
.brk-stud {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: var(--shadow-stud);
vertical-align: middle;
}
.brk-stud--lg { width: 18px; height: 18px; }
Step 5 — Wrap the section/page you want themed with <div data-system="brick">:
<div data-system="brick">
<Button>HELLO BRICK</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Brick.
Install the Wardrobe Brick theme in my Lovable project. Each step below maps to a specific file or change.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;700&display=swap");
/* =========================================================
Wardrobe — BRICK tokens
Hardware/skeumorphic. Embossed panels, raised buttons,
multi-layer shadows. Activate via data-system="brick".
========================================================= */
:where([data-system="brick"]) {
/* ---- Surface ---- */
--color-bg: #C8C9C4; /* outside the device */
--color-casing: #E3E4DF; /* device panel surface */
--color-casing-tint: #DADBD4; /* slightly darker casing for inputs / inset wells */
--color-casing-button: #E8E8E4; /* secondary button surface */
--color-terminal: #DCDDDA; /* inset display screens */
--color-recess: rgba(0, 0, 0, 0.03);
--color-fg: #1A1A1A;
--color-fg-muted: #555555;
--color-fg-label: #6A6B66;
--color-hole: #1C1C1A; /* dark accents / punched holes */
/* ---- Primary palette (LEGO-style functional colors) ---- */
--color-red: #D03027;
--color-red-shade: #9B1C15;
--color-blue: #0055A4;
--color-blue-shade: #00366D;
--color-yellow: #F2A900;
--color-yellow-shade: #B88000;
--color-green: #00853E;
--color-green-shade: #005B2A;
--color-white: #F4F4F2;
--color-white-shade: #BDBDBD;
/* ---- Status (badges) ---- */
--color-status-ok: var(--color-green);
--color-status-proc: var(--color-blue);
--color-status-wait: var(--color-yellow);
--color-status-archive: var(--color-fg-muted);
--color-status-bg: #D1D2CD;
/* ---- Panel gaps + highlights (kritisk for embossed effekt) ---- */
--highlight: rgba(255, 255, 255, 0.7);
--panel-gap-dark: rgba(0, 0, 0, 0.15);
--panel-gap-light: rgba(255, 255, 255, 0.8);
/* ---- Multi-layer shadows (signaturen) ---- */
--shadow-device: 0 20px 50px rgba(0, 0, 0, 0.15),
0 5px 15px rgba(0, 0, 0, 0.05),
inset 1px 1px 2px var(--highlight),
inset -1px -1px 3px rgba(0, 0, 0, 0.1);
--shadow-button-raised: 0 4px 8px rgba(0, 0, 0, 0.2);
--shadow-button-secondary: inset 1px 1px 1px rgba(255, 255, 255, 0.5);
--shadow-button-pressed: inset 1px 1px 2px rgba(0, 0, 0, 0.1);
--shadow-inset-deep: inset 2px 2px 5px rgba(0, 0, 0, 0.05);
--shadow-inset-input: inset 1px 1px 2px rgba(0, 0, 0, 0.1);
--shadow-tile: 4px 4px 10px rgba(0, 0, 0, 0.08),
inset 2px 2px 4px rgba(0, 0, 0, 0.05),
inset -2px -2px 4px rgba(255, 255, 255, 0.5);
--shadow-stud: inset 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-swatch: inset 0 4px 8px rgba(255, 255, 255, 0.3),
inset 0 -4px 8px rgba(0, 0, 0, 0.2),
0 4px 10px rgba(0, 0, 0, 0.15);
/* ---- Radius ---- */
--radius-xs: 2px;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-device: 12px;
--radius-full: 9999px;
/* ---- Typography ---- */
--font-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--text-micro: 10px;
--text-xs: 11px;
--text-sm: 12px;
--text-base: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 18px;
--text-2xl: 24px;
--text-3xl: 32px;
--label-letter-spacing: 0.1em;
--brand-letter-spacing: -0.02em;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
/* ---- Spacing ---- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* ---- Motion ---- */
--duration-fast: 100ms;
--duration-base: 200ms;
--easing: cubic-bezier(0.4, 0, 0.2, 1);
}
[data-system="brick"] {
background: var(--color-bg);
color: var(--color-fg);
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("brk-btn", `brk-btn--${variant}`, `brk-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
numericBadge?: string;
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
export function Input({
size = "md",
variant = "default",
label,
hint,
numericBadge,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
{numericBadge && (
<span
className="brk-field__label"
style={{ position: "absolute", top: 0, right: 0 }}
>
{numericBadge}
</span>
)}
<input
id={inputId}
className={cn(
"brk-input",
`brk-input--${size}`,
variant === "error" && "brk-input--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"brk-field__hint",
variant === "error" && "brk-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
} & Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">;
export function Textarea({
size = "md",
variant = "default",
label,
hint,
className,
id,
...props
}: TextareaProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
<textarea
id={inputId}
className={cn(
"brk-textarea",
`brk-input--${size}`,
variant === "error" && "brk-textarea--error",
className,
)}
aria-invalid={variant === "error" || undefined}
{...props}
/>
{hint && (
<span
className={cn(
"brk-field__hint",
variant === "error" && "brk-field__hint--error",
)}
>
{hint}
</span>
)}
</label>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type SelectProps = {
options: Array<{ value: string; label: string }>;
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
label?: string;
} & Omit<
React.SelectHTMLAttributes<HTMLSelectElement>,
"value" | "onChange" | "size"
>;
export function Select({
options,
value,
onValueChange,
placeholder,
label,
className,
id,
...props
}: SelectProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-field" htmlFor={inputId}>
{label && <span className="brk-field__label">{label}</span>}
<div className="brk-select-wrap">
<select
id={inputId}
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={cn("brk-select", className)}
{...props}
>
{placeholder && (
<option value="" disabled>
{placeholder}
</option>
)}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</label>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "recessed" | "device";
modelNumber?: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
modelNumber,
className,
children,
...props
}: CardProps) {
return (
<div
className={cn(
"brk-card",
variant !== "default" && `brk-card--${variant}`,
className,
)}
{...props}
>
{modelNumber && (
<span className="brk-card__model">{modelNumber}</span>
)}
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "filled" | "ok" | "proc" | "wait" | "archive";
children: React.ReactNode;
className?: string;
};
export function Badge({ variant = "default", children, className }: BadgeProps) {
return (
<span
className={cn(
"brk-badge",
variant !== "default" && `brk-badge--${variant}`,
className,
)}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
modelNumber?: string;
children: React.ReactNode;
};
export function Dialog({
open,
onOpenChange,
title,
modelNumber,
children,
}: DialogProps) {
const dialogRef = React.useRef<HTMLDivElement>(null);
const previousFocus = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
previousFocus.current = document.activeElement as HTMLElement;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
focusable?.[0]?.focus();
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
e.preventDefault();
onOpenChange(false);
return;
}
if (e.key !== "Tab" || !focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKey);
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = prevOverflow;
previousFocus.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="brk-dialog-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div
ref={dialogRef}
className="brk-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="brk-dialog-title"
>
<button
type="button"
className="brk-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
×
</button>
<div className="brk-dialog__head">
<h2 id="brk-dialog-title" className="brk-dialog__title">
{title}
</h2>
{modelNumber && (
<span className="brk-dialog__model">{modelNumber}</span>
)}
</div>
{children}
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
className?: string;
};
export function Tabs({ tabs, activeId, onTabChange, className }: TabsProps) {
return (
<div role="tablist" className={cn("brk-tabs", className)}>
{tabs.map((tab) => (
<button
key={tab.id}
role="tab"
aria-selected={tab.id === activeId}
type="button"
className={cn(
"brk-tabs__btn",
tab.id === activeId && "brk-tabs__btn--active",
)}
onClick={() => onTabChange(tab.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const idx = tabs.findIndex((t) => t.id === activeId);
const next =
e.key === "ArrowRight"
? (idx + 1) % tabs.length
: (idx - 1 + tabs.length) % tabs.length;
onTabChange(tabs[next].id);
}}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
id?: string;
};
export function Switch({ checked, onCheckedChange, label, id }: SwitchProps) {
const inputId = id ?? React.useId();
return (
<label className="brk-switch" htmlFor={inputId}>
<input
id={inputId}
type="checkbox"
role="switch"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
className="brk-switch__input"
/>
<span className="brk-switch__track">
<span className="brk-switch__thumb" />
</span>
{label && <span>{label}</span>}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
export type ToastVariant = "default" | "success" | "error";
export type ToastProps = {
title: string;
description?: string;
variant?: ToastVariant;
timestamp?: string;
};
type ToastEntry = ToastProps & { id: number };
type ToastContextValue = {
show: (toast: ToastProps) => void;
};
const ToastContext = React.createContext<ToastContextValue | null>(null);
export function useToast() {
const ctx = React.useContext(ToastContext);
if (!ctx) throw new Error("useToast must be used within <ToastProvider>");
return ctx;
}
function defaultTime() {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, "0");
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
export function ToastProvider({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
const idRef = React.useRef(0);
const show = React.useCallback((toast: ToastProps) => {
const id = ++idRef.current;
setToasts((prev) => [
...prev,
{ timestamp: defaultTime(), ...toast, id },
]);
window.setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4000);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<div
className="brk-toast-region"
role="region"
aria-label="Notifications"
aria-live="polite"
>
{toasts.map((t) => (
<Toast key={t.id} {...t} />
))}
</div>
</ToastContext.Provider>
);
}
export function Toast({
title,
description,
variant = "default",
timestamp,
}: ToastProps) {
return (
<div
className={cn(
"brk-toast",
variant === "success" && "brk-toast--success",
variant === "error" && "brk-toast--error",
)}
role="status"
>
<span className="brk-toast__title">[ {title} ]</span>
{description && (
<span className="brk-toast__desc">{description}</span>
)}
{timestamp && <span className="brk-toast__time">{timestamp}</span>}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — BRICK globals
Loads Inter + JBM, then component styles.
Only active under data-system="brick".
========================================================= */
[data-system="brick"] *,
[data-system="brick"] *::before,
[data-system="brick"] *::after {
box-sizing: border-box;
}
[data-system="brick"] {
min-height: 100vh;
}
/* ===========================
BUTTON
=========================== */
.brk-btn {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: var(--radius-md);
line-height: 1;
text-decoration: none;
transition: transform var(--duration-fast) var(--easing),
box-shadow var(--duration-fast) var(--easing),
background var(--duration-base) var(--easing);
}
.brk-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.brk-btn--sm { height: 32px; padding: 0 14px; font-size: 11px; }
.brk-btn--md { height: 40px; padding: 0 18px; font-size: 12px; }
.brk-btn--lg { height: 48px; padding: 0 24px; font-size: 13px; }
.brk-btn--primary {
background: var(--color-fg);
color: #FFFFFF;
box-shadow: var(--shadow-button-raised);
}
.brk-btn--primary:not(:disabled):hover {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.25);
}
.brk-btn--primary:not(:disabled):active {
transform: translateY(1px);
box-shadow: var(--shadow-button-pressed);
}
.brk-btn--secondary {
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
box-shadow: var(--shadow-button-secondary);
}
.brk-btn--secondary:not(:disabled):hover {
background: #EEEFEA;
}
.brk-btn--secondary:not(:disabled):active {
background: #DFDFDB;
box-shadow: var(--shadow-button-pressed);
transform: translateY(1px);
}
.brk-btn--destructive {
background: var(--color-red);
color: #FFFFFF;
box-shadow: var(--shadow-button-raised);
}
.brk-btn--destructive:not(:disabled):active {
transform: translateY(1px);
background: var(--color-red-shade);
}
.brk-btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ===========================
FIELD (Input / Textarea / Select wrapper)
=========================== */
.brk-field {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
position: relative;
}
.brk-field__label {
font-family: var(--font-mono);
font-size: var(--text-micro);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
color: var(--color-fg-label);
line-height: 1;
}
.brk-field__hint {
font-family: var(--font-mono);
font-size: var(--text-micro);
color: var(--color-fg-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.brk-field__hint--error { color: var(--color-red); }
/* ===========================
INPUT / TEXTAREA
=========================== */
.brk-input,
.brk-textarea {
font-family: var(--font-mono);
font-size: var(--text-base);
color: var(--color-fg);
background: var(--color-casing-tint);
border: none;
border-bottom: 1px solid transparent;
border-radius: var(--radius-sm);
padding: 8px 12px;
width: 100%;
outline: none;
box-shadow: var(--shadow-inset-input);
transition: border-color var(--duration-fast) var(--easing);
}
.brk-input:focus,
.brk-textarea:focus {
border-bottom-color: var(--color-fg);
}
.brk-input::placeholder,
.brk-textarea::placeholder {
color: var(--color-fg-label);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
}
.brk-input--error,
.brk-textarea--error { border-bottom-color: var(--color-red); }
.brk-input--sm { padding: 6px 10px; font-size: 12px; }
.brk-input--md { padding: 8px 12px; font-size: 13px; }
.brk-input--lg { padding: 12px 14px; font-size: 14px; }
.brk-textarea {
min-height: 100px;
resize: vertical;
}
/* ===========================
SELECT (custom)
=========================== */
.brk-select-wrap {
position: relative;
width: 100%;
}
.brk-select {
font-family: var(--font-mono);
font-size: var(--text-base);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-fg);
background: var(--color-casing-tint);
border: none;
border-radius: var(--radius-sm);
padding: 8px 32px 8px 12px;
width: 100%;
outline: none;
box-shadow: var(--shadow-inset-input);
appearance: none;
-webkit-appearance: none;
cursor: pointer;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%231A1A1A' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
background-repeat: no-repeat;
background-position: right 10px center;
}
/* ===========================
CARD
=========================== */
.brk-card {
background: var(--color-casing);
border: 1px solid rgba(0, 0, 0, 0.05);
border-radius: var(--radius-lg);
padding: var(--space-6);
box-shadow: var(--shadow-tile);
position: relative;
}
.brk-card--recessed {
background: var(--color-recess);
border: 1px solid var(--panel-gap-dark);
box-shadow: 0 1px 0 var(--panel-gap-light);
}
.brk-card--device {
background: var(--color-casing);
box-shadow: var(--shadow-device);
border: none;
border-radius: var(--radius-device);
padding: var(--space-8);
}
.brk-card__model {
position: absolute;
top: 14px;
right: 18px;
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
color: var(--color-fg-muted);
letter-spacing: var(--label-letter-spacing);
text-transform: uppercase;
}
/* ===========================
BADGE (status code)
=========================== */
.brk-badge {
display: inline-flex;
align-items: center;
background: var(--color-status-bg);
color: var(--color-fg);
border-radius: var(--radius-xs);
padding: 2px 6px;
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
text-transform: uppercase;
letter-spacing: 0.05em;
line-height: 1.4;
}
.brk-badge--ok { color: var(--color-status-ok); }
.brk-badge--proc { color: var(--color-status-proc); }
.brk-badge--wait { color: var(--color-status-wait); }
.brk-badge--archive { color: var(--color-status-archive); }
.brk-badge--filled {
background: var(--color-fg);
color: #FFFFFF;
}
/* ===========================
DIALOG
=========================== */
.brk-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 24px;
animation: brk-fade-in 200ms var(--easing);
}
.brk-dialog {
background: var(--color-casing);
border-radius: var(--radius-device);
box-shadow: var(--shadow-device);
max-width: 520px;
width: 100%;
padding: var(--space-8);
position: relative;
animation: brk-pop-in 220ms var(--easing);
}
.brk-dialog__head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin: 0 0 var(--space-5);
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--panel-gap-dark);
box-shadow: 0 1px 0 var(--panel-gap-light);
}
.brk-dialog__title {
font-family: var(--font-display);
font-weight: var(--weight-semibold);
font-size: var(--text-xl);
letter-spacing: var(--brand-letter-spacing);
margin: 0;
}
.brk-dialog__model {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: var(--text-micro);
color: var(--color-fg-muted);
letter-spacing: var(--label-letter-spacing);
text-transform: uppercase;
}
.brk-dialog__close {
position: absolute;
top: 14px;
right: 14px;
width: 28px;
height: 28px;
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-sm);
cursor: pointer;
font-family: var(--font-mono);
font-size: 14px;
line-height: 1;
}
.brk-dialog__close:hover { background: #EEEFEA; }
.brk-dialog__close:active { box-shadow: var(--shadow-button-pressed); }
@keyframes brk-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes brk-pop-in {
from { opacity: 0; transform: translateY(8px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ===========================
TABS
=========================== */
.brk-tabs {
display: inline-flex;
gap: 4px;
flex-wrap: wrap;
}
.brk-tabs__btn {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
padding: 8px 14px;
background: var(--color-casing-button);
color: var(--color-fg);
border: 1px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(255, 255, 255, 0.8);
border-left-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-button-secondary);
cursor: pointer;
line-height: 1;
transition: background var(--duration-fast) var(--easing),
color var(--duration-fast) var(--easing);
}
.brk-tabs__btn:hover { background: #EEEFEA; }
.brk-tabs__btn--active {
background: var(--color-fg);
color: #FFFFFF;
border-color: var(--color-fg);
box-shadow: var(--shadow-button-raised);
}
.brk-tabs__btn:focus-visible {
outline: 2px solid var(--color-fg);
outline-offset: 2px;
}
/* ===========================
SWITCH (physical hardware switch)
=========================== */
.brk-switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
user-select: none;
font-family: var(--font-body);
font-size: var(--text-md);
color: var(--color-fg);
}
.brk-switch__track {
width: 44px;
height: 24px;
background: #D1D2CD;
border-radius: var(--radius-full);
position: relative;
box-shadow: var(--shadow-inset-input);
transition: background var(--duration-base) var(--easing);
flex-shrink: 0;
}
.brk-switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background: var(--color-white);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: transform var(--duration-base) var(--easing);
}
.brk-switch__input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.brk-switch__input:checked + .brk-switch__track {
background: var(--color-green);
}
.brk-switch__input:checked + .brk-switch__track .brk-switch__thumb {
transform: translateX(20px);
}
.brk-switch__input:focus-visible + .brk-switch__track {
outline: 2px solid var(--color-fg);
outline-offset: 3px;
}
/* ===========================
TOAST (status display)
=========================== */
.brk-toast-region {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 1100;
display: flex;
flex-direction: column;
gap: 12px;
pointer-events: none;
width: max-content;
max-width: calc(100vw - 48px);
}
.brk-toast {
background: var(--color-white);
color: var(--color-fg);
border-radius: var(--radius-md);
padding: 14px 18px;
border-left: 3px solid var(--color-fg-muted);
box-shadow: var(--shadow-tile);
font-family: var(--font-body);
font-size: var(--text-base);
pointer-events: auto;
animation: brk-toast-in 280ms var(--easing);
display: flex;
flex-direction: column;
gap: 4px;
min-width: 240px;
max-width: 360px;
position: relative;
}
.brk-toast__title {
font-family: var(--font-mono);
font-weight: var(--weight-bold);
font-size: 11px;
text-transform: uppercase;
letter-spacing: var(--label-letter-spacing);
}
.brk-toast__desc {
font-family: var(--font-body);
font-size: 12px;
color: var(--color-fg-muted);
line-height: 1.4;
}
.brk-toast__time {
position: absolute;
top: 10px;
right: 14px;
font-family: var(--font-mono);
font-size: 10px;
color: var(--color-fg-label);
letter-spacing: 0.05em;
}
.brk-toast--success { border-left-color: var(--color-green); }
.brk-toast--success .brk-toast__title { color: var(--color-green); }
.brk-toast--error { border-left-color: var(--color-red); }
.brk-toast--error .brk-toast__title { color: var(--color-red); }
@keyframes brk-toast-in {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* ===========================
STUDS / SWATCHES (utility)
=========================== */
.brk-stud {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: var(--shadow-stud);
vertical-align: middle;
}
.brk-stud--lg { width: 18px; height: 18px; }
Step 5 — Wrap the section/page you want themed with <div data-system="brick">:
<div data-system="brick">
<Button>HELLO BRICK</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Brick.
Once it's installed, wrap whichever section you want themed:
<div data-system="brick">
<Button>[ EXECUTE ]</Button>
<Card variant="device">…</Card>
</div> Want to see what it looks like in the wild? Baustein →
Got it working?
Drop your email — I'll send you the next theme + occasional builder notes.
No spam. ~1 email per theme drop. Maybe one extra when I ship something interesting.