Make service/job/program cards fully clickable

Use the stretched-link pattern: the card is relative and the title link
gets an after:absolute after:inset-0 overlay so clicking anywhere on the
card navigates to the detail page. Interactive controls (action buttons,
subdomain/Docs links, ProgramActions) are raised with relative z-10 so they
stay independently clickable. Cards also get a hover border for affordance.
This commit is contained in:
2026-06-30 23:07:57 -07:00
parent ab10ca74fe
commit e1a4692a43
3 changed files with 20 additions and 18 deletions

View File

@@ -19,11 +19,11 @@ export function JobCard({ job, health }: JobCardProps) {
} }
return ( return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5"> <div className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 hover:border-[var(--primary)] transition-colors">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<Link <Link
to={`/jobs/${job.id}`} to={`/jobs/${job.id}`}
className="text-base font-semibold hover:text-[var(--primary)] transition-colors" className="text-base font-semibold hover:text-[var(--primary)] transition-colors after:absolute after:inset-0"
> >
{job.id} {job.id}
</Link> </Link>
@@ -59,7 +59,7 @@ export function JobCard({ job, health }: JobCardProps) {
</div> </div>
{job.managed && ( {job.managed && (
<div className="flex items-center gap-1"> <div className="relative z-10 flex items-center gap-1">
{isDown && ( {isDown && (
<button <button
onClick={() => doAction("start")} onClick={() => doAction("start")}

View File

@@ -10,11 +10,11 @@ interface ProgramCardProps {
export function ProgramCard({ program }: ProgramCardProps) { export function ProgramCard({ program }: ProgramCardProps) {
return ( return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5"> <div className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 hover:border-[var(--primary)] transition-colors">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<Link <Link
to={`/programs/${program.id}`} to={`/programs/${program.id}`}
className="text-base font-semibold hover:text-[var(--primary)] transition-colors" className="text-base font-semibold hover:text-[var(--primary)] transition-colors after:absolute after:inset-0"
> >
{program.id} {program.id}
</Link> </Link>
@@ -29,14 +29,16 @@ export function ProgramCard({ program }: ProgramCardProps) {
<p className="text-sm text-[var(--muted)] mb-3">{program.description}</p> <p className="text-sm text-[var(--muted)] mb-3">{program.description}</p>
)} )}
<ProgramActions <div className="relative z-10">
name={program.id} <ProgramActions
actions={program.actions} name={program.id}
active={program.active} actions={program.actions}
behavior={program.behavior} active={program.active}
deployedAs={[...program.services, ...program.jobs]} behavior={program.behavior}
compact deployedAs={[...program.services, ...program.jobs]}
/> compact
/>
</div>
</div> </div>
) )
} }

View File

@@ -22,11 +22,11 @@ export function ServiceCard({ service, health }: ServiceCardProps) {
const isDown = health?.status === "down" const isDown = health?.status === "down"
return ( return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-lg p-5"> <div className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-5 hover:border-[var(--primary)] transition-colors">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<Link <Link
to={`/services/${service.id}`} to={`/services/${service.id}`}
className="text-base font-semibold hover:text-[var(--primary)] transition-colors" className="text-base font-semibold hover:text-[var(--primary)] transition-colors after:absolute after:inset-0"
> >
{service.id} {service.id}
</Link> </Link>
@@ -61,7 +61,7 @@ export function ServiceCard({ service, health }: ServiceCardProps) {
{service.subdomain && ( {service.subdomain && (
<a <a
href={subdomainUrl(service.subdomain) ?? undefined} href={subdomainUrl(service.subdomain) ?? undefined}
className="flex items-center gap-1 text-[var(--primary)] hover:underline" className="relative z-10 flex items-center gap-1 text-[var(--primary)] hover:underline"
> >
<ExternalLink size={12} /> <ExternalLink size={12} />
{service.subdomain} {service.subdomain}
@@ -70,7 +70,7 @@ export function ServiceCard({ service, health }: ServiceCardProps) {
{service.port && ( {service.port && (
<a <a
href={`http://localhost:${service.port}/docs`} href={`http://localhost:${service.port}/docs`}
className="text-[var(--primary)] hover:underline" className="relative z-10 text-[var(--primary)] hover:underline"
> >
Docs Docs
</a> </a>
@@ -78,7 +78,7 @@ export function ServiceCard({ service, health }: ServiceCardProps) {
</div> </div>
{service.managed && ( {service.managed && (
<div className="flex items-center gap-1"> <div className="relative z-10 flex items-center gap-1">
{isDown && ( {isDown && (
<button <button
onClick={() => doAction("start")} onClick={() => doAction("start")}