mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
refactor(ui): migrate hand-rolled tile headers onto TileHeader
Nine insights/cycle cards hand-rolled their icon+title rows at drifting sizes and colours: the sleep trio (debt, chronotype, average) and the glucose clinical panel led with h-4 accent icons, the cycle module (predictions, BBT chart, symptom patterns) coloured its header icons text-primary against both header contracts, and the hypnogram, correlation card, and mood correlation cards carried bespoke title rows. All of them now compose TileHeader. TileHeader grows the two documented gaps instead of a fourth header language: an optional icon (text-only tile titles) and the ONE sanctioned compact variant size="sm" (icon h-4 w-4, title text-sm) used by the dense correlation tiles. The sleep-rhythm guard test now pins the tile-header slot instead of the retired accent icon colour.
This commit is contained in:
1 parent
9a35cfb3e9
commit
8d36e69f75
12 files changed
+149
-129
No files matched your search
@@ -35,7 +35,8 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -150,10 +151,7 @@ export function BbtChart({
|
||||
return (
|
||||
<Card data-slot="cycle-bbt-chart">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Thermometer className="text-primary h-4 w-4" aria-hidden="true" />
|
||||
{t("cycle.bbt.title")}
|
||||
</CardTitle>
|
||||
<TileHeader icon={Thermometer} title={t("cycle.bbt.title")} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!hasCurve ? (
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import { useMemo } from "react";
|
||||
import { Activity } from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { CYCLE_SYMPTOM_CATALOG } from "./symptom-catalog";
|
||||
import { PHASE_HUE } from "./phase-tokens";
|
||||
@@ -56,10 +57,10 @@ export function CycleSymptomPatterns({ rows }: CycleSymptomPatternsProps) {
|
||||
return (
|
||||
<Card data-slot="cycle-symptom-patterns">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Activity className="text-primary h-4 w-4" aria-hidden="true" />
|
||||
{t("cycle.insights.symptomPatternsTitle")}
|
||||
</CardTitle>
|
||||
<TileHeader
|
||||
icon={Activity}
|
||||
title={t("cycle.insights.symptomPatternsTitle")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{rows.length === 0 ? (
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { FERTILE_HUE, FLOW_HUE, OVULATION_HUE } from "./phase-tokens";
|
||||
import type { CyclePrediction, CycleHistoryResponse } from "./types";
|
||||
@@ -64,10 +65,7 @@ export function PredictionsPanel({
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Sparkles className="text-primary h-4 w-4" aria-hidden="true" />
|
||||
{t("cycle.predictions.title")}
|
||||
</CardTitle>
|
||||
<TileHeader icon={Sparkles} title={t("cycle.predictions.title")} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{rawChartMode ? (
|
||||
|
||||
@@ -4,7 +4,8 @@ import dynamic from "next/dynamic";
|
||||
import { Activity, Sparkles } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -116,23 +117,26 @@ export function CorrelationCard({ result }: CorrelationCardProps) {
|
||||
)}
|
||||
/>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
|
||||
<p className="text-muted-foreground text-xs">{subtitle}</p>
|
||||
</div>
|
||||
{result.status === "ok" && (
|
||||
<Badge
|
||||
data-slot="correlation-card-confidence"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"shrink-0 text-xs",
|
||||
CONFIDENCE_BADGE_CLASS[result.confidenceBand.label],
|
||||
)}
|
||||
>
|
||||
{t(CONFIDENCE_LABEL_KEY[result.confidenceBand.label])}
|
||||
</Badge>
|
||||
)}
|
||||
<div className="space-y-0.5">
|
||||
<TileHeader
|
||||
size="sm"
|
||||
title={title}
|
||||
right={
|
||||
result.status === "ok" ? (
|
||||
<Badge
|
||||
data-slot="correlation-card-confidence"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"shrink-0 text-xs",
|
||||
CONFIDENCE_BADGE_CLASS[result.confidenceBand.label],
|
||||
)}
|
||||
>
|
||||
{t(CONFIDENCE_LABEL_KEY[result.confidenceBand.label])}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">{subtitle}</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
|
||||
@@ -4,7 +4,8 @@ import Link from "next/link";
|
||||
import { Activity, Droplet, FlaskConical, TrendingUp } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { InfoHint } from "@/components/ui/info-hint";
|
||||
import { LearningGate } from "@/components/ui/learning-gate";
|
||||
import { LearnMoreLink } from "@/components/ui/learn-more-link";
|
||||
@@ -97,10 +98,10 @@ export function GlucoseClinicalPanel() {
|
||||
return (
|
||||
<Card data-slot="glucose-clinical-panel" data-state="learning">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<Droplet className="h-4 w-4" aria-hidden="true" />
|
||||
{t("insights.bloodGlucose.clinical.learningTitle")}
|
||||
</CardTitle>
|
||||
<TileHeader
|
||||
icon={Droplet}
|
||||
title={t("insights.bloodGlucose.clinical.learningTitle")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LearningGate
|
||||
@@ -134,10 +135,10 @@ export function GlucoseClinicalPanel() {
|
||||
<Card data-slot="glucose-clinical-panel" data-state="asserted">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="flex items-center gap-2 text-base font-semibold">
|
||||
<Droplet className="h-4 w-4" aria-hidden="true" />
|
||||
{t("insights.bloodGlucose.clinical.title")}
|
||||
</CardTitle>
|
||||
<TileHeader
|
||||
icon={Droplet}
|
||||
title={t("insights.bloodGlucose.clinical.title")}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("insights.bloodGlucose.clinical.subtitle", {
|
||||
days: Math.max(1, Math.round(clinical.actualSpanDays)),
|
||||
|
||||
@@ -4,7 +4,8 @@ import dynamic from "next/dynamic";
|
||||
import { Activity } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -99,28 +100,29 @@ function MoodCorrelationCard({
|
||||
className={className}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<CardTitle className="text-sm font-semibold">
|
||||
{t(TITLE_KEY[kind])}
|
||||
</CardTitle>
|
||||
{hasResult && data.result && (
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{t(STRENGTH_KEY[data.result.strength])}
|
||||
</Badge>
|
||||
{/* v1.12.4 (C4) — the "{n} paired days · r" line sat under the
|
||||
scatter as a half-empty row. Move it into an explainer icon
|
||||
in the header so the card footprint stays tight. */}
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.correlation.sourceLabel")}
|
||||
detail={t("insights.mood.correlation.source", {
|
||||
n: data.n,
|
||||
r: data.result.r.toFixed(2),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TileHeader
|
||||
size="sm"
|
||||
title={t(TITLE_KEY[kind])}
|
||||
right={
|
||||
hasResult && data.result ? (
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{t(STRENGTH_KEY[data.result.strength])}
|
||||
</Badge>
|
||||
{/* v1.12.4 (C4) — the "{n} paired days · r" line sat under the
|
||||
scatter as a half-empty row. Move it into an explainer icon
|
||||
in the header so the card footprint stays tight. */}
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.correlation.sourceLabel")}
|
||||
detail={t("insights.mood.correlation.source", {
|
||||
n: data.n,
|
||||
r: data.result.r.toFixed(2),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{hasResult && data.result ? (
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations, useTimeFormatPreference } from "@/lib/i18n/context";
|
||||
import { hourCycleOptions } from "@/lib/format-locale";
|
||||
import { STAGE_COLORS } from "./sleep-stage-stacked-bar";
|
||||
@@ -249,9 +250,7 @@ export function SleepHypnogram({ session }: SleepHypnogramProps) {
|
||||
<Card data-slot="sleep-hypnogram" className="gap-3 md:gap-3">
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.hypnogram.title")}
|
||||
</CardTitle>
|
||||
<TileHeader title={t("insights.sleep.hypnogram.title")} />
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("insights.sleep.hypnogram.subtitle", {
|
||||
asleep: formatMinutes(session.asleepMinutes, locale),
|
||||
|
||||
@@ -80,8 +80,10 @@ describe("<SleepDebtCard>", () => {
|
||||
for (const html of [partial, ready]) {
|
||||
expect(html).not.toContain("gap-2 py-4 md:gap-2 md:py-4");
|
||||
expect(html).toContain("md:py-6");
|
||||
// Semantic info token, not the raw Dracula cyan.
|
||||
expect(html).toContain("text-info");
|
||||
// Header rides the canonical TileHeader (foreground icon), never a
|
||||
// hand-rolled accent-coloured row or the raw Dracula cyan.
|
||||
expect(html).toContain('data-slot="tile-header"');
|
||||
expect(html).not.toContain("text-info");
|
||||
expect(html).not.toContain("text-dracula-cyan");
|
||||
}
|
||||
});
|
||||
@@ -123,11 +125,12 @@ describe("<ChronotypeCard>", () => {
|
||||
expect(html).toContain("text-2xl");
|
||||
});
|
||||
|
||||
it("uses the semantic info token, never the raw Dracula cyan", () => {
|
||||
it("renders the canonical TileHeader, never the raw Dracula cyan", () => {
|
||||
const learning = render(<ChronotypeCard chronotype={LEARNING_CHRONO} />);
|
||||
const ready = render(<ChronotypeCard chronotype={READY_CHRONO} />);
|
||||
for (const html of [learning, ready]) {
|
||||
expect(html).toContain("text-info");
|
||||
expect(html).toContain('data-slot="tile-header"');
|
||||
expect(html).not.toContain("text-info");
|
||||
expect(html).not.toContain("text-dracula-cyan");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { BedDouble } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { LearningGate } from "@/components/ui/learning-gate";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import type { AverageSleepDto } from "./use-sleep-rhythm";
|
||||
|
||||
/** Whole hours + minutes from a minute total, for the headline figure. */
|
||||
@@ -35,12 +36,10 @@ export function AverageSleepCard({ average }: { average: AverageSleepDto }) {
|
||||
return (
|
||||
<Card data-slot="average-sleep-card">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<BedDouble className="text-info h-4 w-4" />
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.average.title")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<TileHeader
|
||||
icon={BedDouble}
|
||||
title={t("insights.sleep.average.title")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LearningGate
|
||||
@@ -59,12 +58,10 @@ export function AverageSleepCard({ average }: { average: AverageSleepDto }) {
|
||||
return (
|
||||
<Card data-slot="average-sleep-card">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<BedDouble className="text-info h-4 w-4" />
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.average.title")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<TileHeader
|
||||
icon={BedDouble}
|
||||
title={t("insights.sleep.average.title")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-0.5">
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useState } from "react";
|
||||
import { Clock, ChevronDown, ChevronUp } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { LearningGate } from "@/components/ui/learning-gate";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import type { ChronotypeDto } from "./use-sleep-rhythm";
|
||||
|
||||
/** Minutes-of-day → "HH:MM" wall-clock label (24 h, zero-padded). */
|
||||
@@ -48,12 +49,10 @@ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) {
|
||||
return (
|
||||
<Card data-slot="chronotype-card">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="text-info h-4 w-4" />
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.chronotype.title")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<TileHeader
|
||||
icon={Clock}
|
||||
title={t("insights.sleep.chronotype.title")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LearningGate
|
||||
@@ -74,12 +73,7 @@ export function ChronotypeCard({ chronotype }: { chronotype: ChronotypeDto }) {
|
||||
return (
|
||||
<Card data-slot="chronotype-card">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="text-info h-4 w-4" />
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.chronotype.title")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<TileHeader icon={Clock} title={t("insights.sleep.chronotype.title")} />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Uniform with the sleep-debt tile: a bold primary value (the band) at
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { Moon } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { InfoHint } from "@/components/ui/info-hint";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { LearningGate } from "@/components/ui/learning-gate";
|
||||
import type { SleepDebtDto } from "./use-sleep-rhythm";
|
||||
|
||||
@@ -33,19 +34,19 @@ export function SleepDebtCard({ debt }: { debt: SleepDebtDto }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="text-info h-4 w-4" />
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.debt.title")}
|
||||
</CardTitle>
|
||||
{/* v1.25.0 — when the active source on the user's sleep-debt ladder
|
||||
is our own COMPUTED engine (every user today, until a provider
|
||||
ships a native debt), explain what the figure means and that it
|
||||
differs from a wearable's native number. */}
|
||||
{debt.source === "COMPUTED" ? (
|
||||
<InfoHint label={t("insights.sleep.debt.computedInfo")} />
|
||||
) : null}
|
||||
</div>
|
||||
{/* v1.25.0 — when the active source on the user's sleep-debt ladder
|
||||
is our own COMPUTED engine (every user today, until a provider
|
||||
ships a native debt), explain what the figure means and that it
|
||||
differs from a wearable's native number. */}
|
||||
<TileHeader
|
||||
icon={Moon}
|
||||
title={t("insights.sleep.debt.title")}
|
||||
right={
|
||||
debt.source === "COMPUTED" ? (
|
||||
<InfoHint label={t("insights.sleep.debt.computedInfo")} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LearningGate
|
||||
@@ -65,19 +66,19 @@ export function SleepDebtCard({ debt }: { debt: SleepDebtDto }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="text-info h-4 w-4" />
|
||||
<CardTitle className="text-base font-semibold">
|
||||
{t("insights.sleep.debt.title")}
|
||||
</CardTitle>
|
||||
{/* v1.25.0 — the resolved full-debt card is the common case and the
|
||||
place the figure most needs context: explain the COMPUTED engine
|
||||
and that it differs from a wearable's native number here too, not
|
||||
only in the still-learning `partial` state above. */}
|
||||
{debt.source === "COMPUTED" ? (
|
||||
<InfoHint label={t("insights.sleep.debt.computedInfo")} />
|
||||
) : null}
|
||||
</div>
|
||||
{/* v1.25.0 — the resolved full-debt card is the common case and the
|
||||
place the figure most needs context: explain the COMPUTED engine
|
||||
and that it differs from a wearable's native number here too, not
|
||||
only in the still-learning `partial` state above. */}
|
||||
<TileHeader
|
||||
icon={Moon}
|
||||
title={t("insights.sleep.debt.title")}
|
||||
right={
|
||||
debt.source === "COMPUTED" ? (
|
||||
<InfoHint label={t("insights.sleep.debt.computedInfo")} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-0.5">
|
||||
|
||||
@@ -30,13 +30,23 @@ interface TileHeaderProps {
|
||||
* Leading glyph. Accepts a Lucide icon component (or any component that
|
||||
* takes a `className`) so the caller passes the component itself
|
||||
* (`icon={Target}`), not a pre-sized node — `TileHeader` owns the size
|
||||
* and colour so every tile header matches.
|
||||
* and colour so every tile header matches. Optional: a text-only tile
|
||||
* header (chart cards whose title needs no glyph) omits it and still
|
||||
* gets the canonical row + `CardTitle` treatment.
|
||||
*/
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
icon?: ComponentType<{ className?: string }>;
|
||||
/** Heading text (or node). Rendered inside a `CardTitle` at `text-base`. */
|
||||
title: ReactNode;
|
||||
/** Optional trailing affordance pinned to the right edge of the row. */
|
||||
right?: ReactNode;
|
||||
/**
|
||||
* The ONE sanctioned compact variant: icon `h-4 w-4`, title `text-sm` —
|
||||
* for dense correlation/stat tiles that sit several to a row where the
|
||||
* full `text-base` header visually overpowers the tile body. Everything
|
||||
* else stays on the default; do not express a third size via
|
||||
* `titleClassName`.
|
||||
*/
|
||||
size?: "default" | "sm";
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
/** Optional id on the `CardTitle`, e.g. for an `aria-labelledby` link. */
|
||||
@@ -47,6 +57,7 @@ export function TileHeader({
|
||||
icon: Icon,
|
||||
title,
|
||||
right,
|
||||
size = "default",
|
||||
className,
|
||||
titleClassName,
|
||||
id,
|
||||
@@ -58,8 +69,19 @@ export function TileHeader({
|
||||
>
|
||||
{/* Icon and heading share the foreground colour — the calm,
|
||||
high-contrast read the "Einschätzung" card established. */}
|
||||
<Icon className="text-foreground h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<CardTitle id={id} className={cn("text-base", titleClassName)}>
|
||||
{Icon ? (
|
||||
<Icon
|
||||
className={cn(
|
||||
"text-foreground shrink-0",
|
||||
size === "sm" ? "h-4 w-4" : "h-5 w-5",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<CardTitle
|
||||
id={id}
|
||||
className={cn(size === "sm" ? "text-sm" : "text-base", titleClassName)}
|
||||
>
|
||||
{title}
|
||||
</CardTitle>
|
||||
{right ? <div className="ml-auto flex items-center">{right}</div> : null}
|
||||
|
||||
Reference in new issue
Block a user