fix(cycle): restore the wellness-tile entrance so the ring renders

The wheel tile, phase-education card, and dashboard cycle tile each set
both data-revealed and .wellness-tile-rise on the same node. The keyframe
selector is [data-revealed="true"] .wellness-tile-rise (descendant
combinator), so it never matched a same-node pair — the tiles stayed at
the rise rule's opacity: 0, invisible but still boxed. Lift data-revealed
onto a contents wrapper (the documented pattern from the cycle insight
summary card) so the entrance plays and the signature ring paints.
This commit is contained in:
Marc-André Bombeck committed 2026-06-16 16:52:03 +02:00
1 parent f0614a8080
commit 3bbc0016d9
3 files changed
+64 -42

No files matched your search

+24 -18
View File
@@ -92,23 +92,28 @@ export function CycleRingTile({ className }: { className?: string }) {
const hue = PHASE_HUE[wheel.phase];
return (
<Link
href="/cycle"
data-slot="wellness-cycle-tile"
data-metric="CYCLE"
data-phase={wheel.phase}
data-revealed={play ? "true" : undefined}
// Match the score `RingTile` chrome: the phase's `--tile-hue` low-opacity
// mix over `--card` + the faint film grain (the `.wellness-tile` family),
// with a calm once-per-session rise. The per-phase hue arc inside the
// ring is the only saturated thing on the tile.
style={{ "--tile-hue": hue } as React.CSSProperties}
className={cn(
"wellness-tile focus-visible:ring-ring flex flex-col gap-4 rounded-xl p-5 focus-visible:ring-2 focus-visible:outline-none",
play && "wellness-tile-rise",
className,
)}
>
// `data-revealed` must sit on an ANCESTOR of `.wellness-tile-rise` — the
// keyframe selector is `[data-revealed="true"] .wellness-tile-rise`
// (descendant combinator). Both on the same node never matched, leaving the
// tile stuck at the rise rule's `opacity: 0`. The `contents` wrapper carries
// the flag without adding a layout box.
<div data-revealed={play ? "true" : undefined} className="contents">
<Link
href="/cycle"
data-slot="wellness-cycle-tile"
data-metric="CYCLE"
data-phase={wheel.phase}
// Match the score `RingTile` chrome: the phase's `--tile-hue`
// low-opacity mix over `--card` + the faint film grain (the
// `.wellness-tile` family), with a calm once-per-session rise. The
// per-phase hue arc inside the ring is the only saturated thing.
style={{ "--tile-hue": hue } as React.CSSProperties}
className={cn(
"wellness-tile focus-visible:ring-ring flex flex-col gap-4 rounded-xl p-5 focus-visible:ring-2 focus-visible:outline-none",
play && "wellness-tile-rise",
className,
)}
>
<div className="text-foreground flex items-center gap-2">
<Droplets className="h-5 w-5 shrink-0" aria-hidden="true" />
<span className="truncate text-base leading-none font-semibold">
@@ -131,6 +136,7 @@ export function CycleRingTile({ className }: { className?: string }) {
{t(`cycle.phase.${wheel.phase}`)}
</span>
</div>
</Link>
</Link>
</div>
);
}
+21 -11
View File
@@ -159,20 +159,29 @@ export function CycleView() {
<div className="grid gap-6 lg:grid-cols-[minmax(0,22rem)_minmax(0,1fr)] lg:items-start">
{/* Wheel + compact phase card — grouped so they stick together. */}
<div className="flex flex-col gap-4 lg:sticky lg:top-6">
{/* Wheel — the signature ring on the premium wellness-tile surface. */}
{/* Wheel — the signature ring on the premium wellness-tile surface.
`data-revealed` must sit on an ANCESTOR of `.wellness-tile-rise`
— the keyframe selector is `[data-revealed="true"]
.wellness-tile-rise` (descendant combinator). Both on the same
node never matched, leaving the tile stuck at the rise rule's
`opacity: 0` (invisible but still boxed). The `contents` wrapper
carries the flag without adding a layout box. */}
<div
data-slot="cycle-wheel-tile"
data-revealed={play ? "true" : undefined}
style={
tileHue
? ({ "--tile-hue": tileHue } as React.CSSProperties)
: undefined
}
className={cn(
"wellness-tile flex flex-col items-center gap-3 rounded-xl px-6 py-6",
play && "wellness-tile-rise",
)}
className="contents"
>
<div
data-slot="cycle-wheel-tile"
style={
tileHue
? ({ "--tile-hue": tileHue } as React.CSSProperties)
: undefined
}
className={cn(
"wellness-tile flex flex-col items-center gap-3 rounded-xl px-6 py-6",
play && "wellness-tile-rise",
)}
>
{loading ? (
<div className="flex h-[220px] items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
@@ -216,6 +225,7 @@ export function CycleView() {
{t("cycle.ring.firstPeriodCta")}
</Button>
) : null}
</div>
</div>
{/* Phase-education card beneath the wheel — the single instance,
+19 -13
View File
@@ -107,18 +107,23 @@ export function PhaseEducationCard({
const phaseName = phase ? t(`cycle.phase.${phase}`) : null;
return (
<section
data-slot="cycle-phase-education"
data-revealed={animate ? "true" : undefined}
data-phase={phase ?? "none"}
aria-label={t("cycle.phaseEducation.title")}
style={{ "--tile-hue": hue } as React.CSSProperties}
className={cn(
"wellness-tile rounded-xl px-5 py-5",
animate && "wellness-tile-rise",
className,
)}
>
// `data-revealed` must sit on an ANCESTOR of `.wellness-tile-rise` — the
// keyframe selector is `[data-revealed="true"] .wellness-tile-rise`
// (descendant combinator). Both on the same node never matched, leaving the
// tile stuck at the rise rule's `opacity: 0`. The `contents` wrapper carries
// the flag without adding a layout box.
<div data-revealed={animate ? "true" : undefined} className="contents">
<section
data-slot="cycle-phase-education"
data-phase={phase ?? "none"}
aria-label={t("cycle.phaseEducation.title")}
style={{ "--tile-hue": hue } as React.CSSProperties}
className={cn(
"wellness-tile rounded-xl px-5 py-5",
animate && "wellness-tile-rise",
className,
)}
>
{/* Zone 1 — eyebrow + phase name with a hue dot (never colour-only). */}
<div className="flex items-center gap-2">
<Sparkles
@@ -196,6 +201,7 @@ export function PhaseEducationCard({
) : null}
</>
)}
</section>
</section>
</div>
);
}