- 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>
64 lines
1.6 KiB
TypeScript
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 };
|