fix(targets): make the coach launch an icon-only affordance

The per-card Coach CTA was rendering a labelled ghost pill (chat
glyph + 'Coach fragen' text). On a card already carrying a status
pill, range bar, consistency strip, headline number, edit cog and a
source link, the labelled pill dominated the optical hierarchy. Drop
the visible label, keep the chat-bubble glyph, and surface the
affordance via aria-label + title so screen readers and tooltips
still announce 'Ask Coach' / 'Coach fragen'. Collapse the footer
flex container to a single horizontal row at every breakpoint now
that the button is icon-sized.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 01:45:03 +02:00
1 parent d286220bed
commit 1b0e81ae42
3 files changed
+31 -15

No files matched your search

@@ -28,7 +28,13 @@ describe("<TargetCoachButton>", () => {
it("renders the button when aiEnabled is true", () => {
const html = render({ ...baseProps, aiEnabled: true });
expect(html).toContain('data-slot="target-coach-cta"');
expect(html).toContain("Ask Coach");
// v1.4.28 FB-L1 — the affordance is now icon-only; the label
// survives as an aria-label + title so screen readers + tooltips
// still surface "Ask Coach" / "Coach fragen".
expect(html).toContain('aria-label="Ask Coach"');
expect(html).toContain('title="Ask Coach"');
// No visible text label — only the chat-bubble glyph.
expect(html).not.toMatch(/>Ask Coach</);
});
it("renders NOTHING when aiEnabled is false (no broken-button state)", () => {
+9 -10
View File
@@ -656,20 +656,19 @@ export function TargetCard({
</div>
)}
{/* Row 6: footer. Mobile-first reflow:
- default (<640px): stack vertically; the Coach CTA
spans full width so the touch target is generous, the
source link sits beneath it right-aligned.
- sm+ (≥640px): horizontal row, Coach left + source right.
`mt-auto` pins to card bottom so grid cells align. */}
<div className="mt-auto flex flex-col items-stretch gap-2 pt-1 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
{/* Row 6: footer. v1.4.28 FB-L1 — the Coach CTA is now an
icon-only affordance (chat-bubble glyph + aria-label), so
the row stays horizontal at every breakpoint: icon left,
source link right. `mt-auto` pins to card bottom so grid
cells align. */}
<div className="mt-auto flex flex-row items-center justify-between gap-3 pt-1">
{aiEnabled && (
<TargetCoachButton
prefill={coachPrefill}
sources={coachSources}
onAskCoach={onAskCoach}
aiEnabled={aiEnabled}
className="w-full justify-center sm:-ml-2 sm:w-auto sm:justify-start"
className="-ml-2"
/>
)}
{sourceLink ? (
@@ -677,13 +676,13 @@ export function TargetCard({
href={sourceLink}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 self-end text-xs sm:ml-auto sm:self-auto"
className="text-muted-foreground hover:text-foreground ml-auto inline-flex items-center gap-1 text-xs"
>
<span>{target.source}</span>
<ExternalLink className="size-3" aria-hidden="true" />
</a>
) : (
<span className="text-muted-foreground self-end text-xs sm:ml-auto sm:self-auto">
<span className="text-muted-foreground ml-auto text-xs">
{target.source}
</span>
)}
+15 -4
View File
@@ -1,6 +1,6 @@
"use client";
import { Sparkles } from "lucide-react";
import { MessageCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
@@ -17,6 +17,14 @@ import type { CoachScope, CoachScopeSource } from "@/lib/ai/coach/types";
* On click the button fires `onAskCoach({ prefill, scope })`. The
* parent owns the `CoachDrawer` state; this button doesn't know about
* the drawer at all.
*
* v1.4.28 R3c FB-L1 — collapsed to an icon-only affordance. The card
* footer was carrying a labelled pill ("Coach fragen"); on a card with
* a status pill, range bar, consistency strip, headline number, edit
* cog and a source link, the labelled pill dominated the visual
* hierarchy. Dropping the label retains the affordance via the
* chat-bubble glyph + `aria-label` while shrinking the optical weight
* to a per-card icon button.
*/
export interface TargetCoachButtonProps {
/**
@@ -51,11 +59,13 @@ export function TargetCoachButton({
if (!aiEnabled) return null;
const accessibleLabel = t("targets.coach.cta");
return (
<Button
type="button"
variant="ghost"
size="sm"
size="icon"
onClick={() =>
onAskCoach({
prefill,
@@ -66,10 +76,11 @@ export function TargetCoachButton({
})
}
data-slot="target-coach-cta"
aria-label={accessibleLabel}
title={accessibleLabel}
className={className}
>
<Sparkles className="size-3.5" aria-hidden="true" />
<span>{t("targets.coach.cta")}</span>
<MessageCircle className="size-4" aria-hidden="true" />
</Button>
);
}