Files
Containr/app/frontend/src/shared/components/Card.tsx
T
Tomáš DvořákandDevin 9271d0e3ac feat(ui): full frontend redesign to the Sentry-style design system (#86)
* feat(ui): full frontend redesign to the Sentry-style design system

Replace the legacy UI with the dark, mono-accent design language from
design/23-sentry.html across every non-canvas surface.

- PlatformShell: fixed 252px sidebar with grouped nav (Platform /
  Operate / Workspace), brand mark, user card, and a topbar with
  search (Cmd+K palette), env badge, upgrade, notifications, and
  theme toggle; mobile collapses to a top bar with horizontal nav
- New shared component kit in shared/components/sentry.tsx:
  SPageHead, SStat, SCard, STable, SSection, SPill, Ticks, Seg,
  GhostBtn/QuietBtn/IconBtn, status tone helpers
- New DashboardPage at / — stats row, per-node CPU chart with
  1h/24h/7d ranges, resource usage tick strips, anomaly box,
  node summary table, recent deployments and project cards;
  real API data with demo-mode fixtures
- Landing page moved to /landing and rebuilt: live host telemetry
  window, install strip, feature grid, template catalog, docs
- Every page (Projects, Templates, Builds, Databases, HA,
  Operations, Activity, Security, Usage, Docs, People, Settings,
  Audit Logs, Webhooks, Admin, NodeDetail, ServiceDetail, auth,
  setup, not-found) migrated to the new kit and tokens
- Nav active state resolves by longest matching href
- Canvas internals intentionally untouched — separate pass

Verified: tsc --noEmit, eslint, vite build, Playwright walkthrough
of every route at desktop and mobile widths (demo + live session).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): deep audit pass — palette navigation, crash fix, honest badges

- CommandPalette: full route navigation, project deep-links fetched on
  open, Create group only where onAddService is wired, dead Connect
  items removed, decorative kbd hints dropped; workspace passes its own
  view targets
- NodeDetailPage crash: container status arrives as a Docker state
  object, not a string — flattened in listAgentContainers and guarded
  at render for persisted-cache payloads
- env badge reflects import.meta.env.MODE instead of hardcoded
  'production'
- Relative times capped: >1y or future timestamps render 'n/a'/'—'
  (zero-time heartbeats no longer show 739898d ago)
- Post-auth and setup redirects land on / (dashboard is the index)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(ui): use SPageHead actions prop, satisfy tsc -b literal types

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-10 17:20:44 +02:00

95 lines
2.8 KiB
TypeScript

import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
type CardVariant = 'default' | 'elevated' | 'bordered' | 'ghost' | 'glass';
type CardProps = HTMLAttributes<HTMLDivElement> & {
variant?: CardVariant;
padding?: 'none' | 'sm' | 'md' | 'lg';
header?: ReactNode;
footer?: ReactNode;
hoverable?: boolean;
};
const variantStyles: Record<CardVariant, string> = {
default: 'bg-[var(--surface-card)] border border-[var(--border-subtle)]',
elevated: 'bg-[var(--surface-card)] shadow-lg shadow-black/40',
bordered: 'bg-transparent border border-[var(--border-default)]',
ghost: 'bg-[var(--surface-muted)]/50',
glass: 'bg-[var(--surface-card)]/80 backdrop-blur-xl border border-[var(--border-subtle)]',
};
const paddingStyles: Record<'none' | 'sm' | 'md' | 'lg', string> = {
none: '',
sm: 'p-3',
md: 'p-4 md:p-5',
lg: 'p-6 md:p-8',
};
export const Card = forwardRef<HTMLDivElement, CardProps>(
(
{
variant = 'default',
padding = 'md',
header,
footer,
hoverable = false,
className = '',
children,
...props
},
ref
) => {
return (
<div
ref={ref}
className={`
rounded-[var(--radius-lg)]
${variantStyles[variant]}
${paddingStyles[padding]}
${hoverable ? 'transition-all duration-200 hover:border-[var(--border-default)] hover:shadow-xl hover:shadow-black/30 hover:-translate-y-0.5' : ''}
${className}
`}
{...props}
>
{header && (
<div className="mb-4 pb-4 border-b border-[var(--border-subtle)]">{header}</div>
)}
{children}
{footer && (
<div className="mt-4 pt-4 border-t border-[var(--border-subtle)]">{footer}</div>
)}
</div>
);
}
);
Card.displayName = 'Card';
type CardHeaderProps = HTMLAttributes<HTMLDivElement> & {
title: string;
description?: string;
icon?: ReactNode;
action?: ReactNode;
};
export function CardHeader({ title, description, icon, action, className = '', ...props }: CardHeaderProps) {
return (
<div className={`flex items-start justify-between gap-4 ${className}`} {...props}>
<div className="flex items-start gap-3">
{icon && (
<div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ring-1 ring-[var(--tint-05)]" style={{ background: 'var(--tint-07)' }}>
{icon}
</div>
)}
<div>
<h3 className="font-headline text-base font-semibold text-[var(--text-primary)] tracking-tight">{title}</h3>
{description && (
<p className="text-sm text-[var(--text-secondary)] mt-0.5">{description}</p>
)}
</div>
</div>
{action && <div className="flex-shrink-0">{action}</div>}
</div>
);
}