Files
commitea/apps/desktop/src/renderer/src/components/ui/card.tsx
Croissant Le Doux 635025113c feat(desktop): port the 15 design-system primitives (#14)
Verbatim port of the handoff primitives into components/ui/ — Icon,
Button, IconButton, Badge, Tag, Card, Tabs (core); Input, Select,
Checkbox, Radio, Switch (forms); Dialog, Toast, Tooltip (feedback).
Each keeps its injected token-referencing CSS byte-for-byte; the
handoff .d.ts contracts become the exported prop interfaces. Barrel at
components/ui/index.ts.

Adds a PrimitivesGallery (app root for now; real shell is P3-2) that
exercises every primitive with a light/dark toggle. Smoke suite asserts
the gallery, section coverage, theme flip, and dialog open/Escape;
screenshots captured for both themes. typecheck + 5 e2e green.

Closes P3-1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:12:00 -04:00

80 lines
2.6 KiB
TypeScript

import React from 'react';
/**
* Paper card — hairline border, 10px radius, whispered shadow. `jade` adds the jade top rule
* reserved for the view's key card (e.g. the Focus card).
*/
export interface CardProps {
/** Caslon title */
title?: React.ReactNode;
/** Small uppercase label above the title */
overline?: string;
/** Right-aligned header actions (IconButtons) */
actions?: React.ReactNode;
/** Footer row below a hairline */
footer?: React.ReactNode;
/** Jade top rule — one per view @default false */
jade?: boolean;
/** Remove body padding (tables, charts) @default false */
flush?: boolean;
children?: React.ReactNode;
style?: React.CSSProperties;
}
const CSS = `
.ct-card {
background: var(--surface-card);
border: 1px solid var(--border-hairline);
border-radius: var(--radius-3);
box-shadow: var(--shadow-1);
}
.ct-card--jade { box-shadow: var(--shadow-jade-line), var(--shadow-1); }
.ct-card__header {
display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
padding: 16px 20px 0;
}
.ct-card__title {
font: var(--text-title);
color: var(--ink-1);
margin: 0;
}
.ct-card__overline {
font: var(--text-overline);
letter-spacing: var(--letter-spacing-wide);
text-transform: uppercase;
color: var(--ink-3);
margin: 0 0 6px;
}
.ct-card__body { padding: 14px 20px 18px; }
.ct-card__body--flush { padding: 0; }
.ct-card__footer {
display: flex; align-items: center; gap: 8px;
padding: 12px 20px;
border-top: 1px solid var(--border-hairline);
}
`;
(function inject() {
if (typeof document !== 'undefined' && !document.getElementById('ct-card-css')) {
const s = document.createElement('style'); s.id = 'ct-card-css'; s.textContent = CSS;
document.head.appendChild(s);
}
})();
export function Card({ title, overline, actions, footer, jade = false, flush = false, children, style, ...rest }: CardProps) {
return (
<section className={`ct-card${jade ? ' ct-card--jade' : ''}`} style={style} {...rest}>
{(title || overline || actions) ? (
<header className="ct-card__header">
<div>
{overline ? <p className="ct-card__overline">{overline}</p> : null}
{title ? <h2 className="ct-card__title">{title}</h2> : null}
</div>
{actions ? <div style={{ display: 'flex', gap: '4px', flexShrink: 0 }}>{actions}</div> : null}
</header>
) : null}
<div className={`ct-card__body${flush ? ' ct-card__body--flush' : ''}`}>{children}</div>
{footer ? <footer className="ct-card__footer">{footer}</footer> : null}
</section>
);
}