Files
ui/src/code-block.tsx
T
enricobuehlerandClaude Fable 5 67c4b2fd91 feat: add Badge, Spinner, Skeleton, Switch, Table, EmptyState, CodeBlock primitives
- badge: cva variants default/secondary/outline/success/warn/error as
  bg-<tone>/15 text-<tone> tints, sm|default sizes, dot/pulse props
- spinner: the lens spinner graduated from the punktfunk console,
  genericized to var(--brand)/var(--brand-light); reduced-motion honored
- skeleton: Skeleton + SkeletonText pulse placeholders
- form/switch: radix Switch with motion layout-spring thumb,
  useSound("toggle"), useControllableState, material-aware track
  (new switch key in MaterialTheme)
- table: Table/TableHeader/TableBody/TableRow/TableHead/TableCell
  graduated from the console (muted tokens mapped to
  neutral-accent/secondary, which exist in this theme)
- empty-state: dashed-border centered column, error variant, icon
  entrance via new emptyStateIcon animation token + 0.08 stagger
- code-block: mono pre with optional clipboard copy button (Copy→Check)
- theme.css/globals.css: new --warn and --brand-light tones
  (+ --color-warn/--color-brand-light Tailwind mappings)
- CSF3 stories for every new primitive; subpath exports for all of them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:34:11 +02:00

64 lines
1.6 KiB
TypeScript

"use client";
import { Check, Copy } from "lucide-react";
import { type FC, useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
export type CodeBlockProps = {
/** The code to display (and copy). */
children: string;
/** Show a copy-to-clipboard button in the top-right corner. */
copy?: boolean;
className?: string;
};
const CodeBlock: FC<CodeBlockProps> = ({
children,
copy = false,
className,
}) => {
const [copied, setCopied] = useState(false);
const resetRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(
() => () => {
if (resetRef.current) clearTimeout(resetRef.current);
},
[],
);
const onCopy = async () => {
try {
await navigator.clipboard.writeText(children);
setCopied(true);
if (resetRef.current) clearTimeout(resetRef.current);
resetRef.current = setTimeout(() => setCopied(false), 1500);
} catch {
// Clipboard unavailable (permissions / insecure context) — ignore.
}
};
return (
<div data-slot="code-block" className={cn("relative", className)}>
<pre className="overflow-x-auto rounded-lg bg-neutral-accent p-3 font-mono text-main text-xs">
<code>{children}</code>
</pre>
{copy && (
<button
type="button"
aria-label={copied ? "Copied" : "Copy to clipboard"}
onClick={onCopy}
className="absolute top-1.5 right-1.5 rounded-md p-1.5 text-secondary transition-colors hover:bg-neutral-highlight/40 hover:text-main"
>
{copied ? (
<Check className="size-3.5 text-success" aria-hidden="true" />
) : (
<Copy className="size-3.5" aria-hidden="true" />
)}
</button>
)}
</div>
);
};
export { CodeBlock };