1import React from "react";
2
3type Theme = "light" | "dark";
4
5interface CardShellProps {
6 theme?: Theme;
7 /** Controls and context: actions, attribution, deltas. Lives on the chassis. */
8 rail?: React.ReactNode;
9 /** "actions" for buttons; "text" for plain context, indented to the face's text edge */
10 railKind?: "actions" | "text";
11 as?: "div" | "figure" | "article";
12 className?: string;
13 faceClassName?: string;
14 children: React.ReactNode;
15}
16
17/* Every card is built like every other object in the kit: a chassis (bezel)
18 holding a face. Content goes on the face; controls and context go on the
19 rail, the strip of chassis below it. The two are nearly the same tone —
20 the hairline around the face is what separates them.
21
22 Geometry, measured from the card's outer edge:
23 - chassis 24px radius, 6px bezel → face 18px radius (concentric)
24 - rail content sits 12px in from the left and bottom, 10px below the face
25 - a button's label then starts 30px in — the same edge as the face's text,
26 and plain-text rails are indented to that edge too */
27export function CardShell({
28 theme = "light",
29 rail,
30 railKind = "actions",
31 as: Tag = "div",
32 className = "",
33 faceClassName = "",
34 children,
35}: CardShellProps) {
36 const dark = theme === "dark";
37
38 return (
39 <Tag
40 className={[
41 "rim relative m-0 rounded-[24px] p-1.5 flex flex-col h-full",
42 rail ? "pb-0" : "",
43 className,
44 ].join(" ")}
45 style={{
46 background: dark ? "var(--card-bezel-dark)" : "var(--card-bezel-light)",
47 boxShadow: dark ? "var(--card-dark-shadow)" : "var(--card-light-shadow)",
48 ["--rim" as string]: dark ? "rgba(255,255,255,0.1)" : "rgba(255,255,255,1)",
49 } as React.CSSProperties}
50 >
51 <div
52 className={["relative flex-1 min-h-0 rounded-[18px] overflow-hidden flex flex-col", faceClassName].join(" ")}
53 style={{
54 background: dark ? "var(--card-dark)" : "var(--card-light)",
55 boxShadow: dark ? "var(--card-face-dark-shadow)" : "var(--card-face-light-shadow)",
56 }}
57 >
58 {children}
59 </div>
60
61 {rail && (
62 <div className="relative shrink-0 px-1.5 pt-2.5 pb-3">
63 <div
64 className={[
65 // 40px = a small button's full height, so text rails and button
66 // rails come out the same height and line up across a row
67 "min-h-10 flex items-center gap-2 text-[12.5px]",
68 railKind === "text" ? "pl-[18px] pr-3 gap-2.5" : "",
69 dark ? "text-neutral-400" : "text-neutral-500",
70 ].join(" ")}
71 >
72 {rail}
73 </div>
74 </div>
75 )}
76 </Tag>
77 );
78}