fix(insights): MED-bucket polish across coach + report surfaces

- weekly-report eyebrow chip drops uppercase + tracking-wide for the
  sentence-case rule the rest of the app follows; print stylesheet
  now neutralises bg + colour so monochrome output reads clean
  (Design-M4, Design-L3).

- correlation-card "Try a 7-day experiment" CTA flips from default to
  outline while disabled. Disabled-primary read as "the system is
  broken right now"; outline reads as "this is on the roadmap"
  (Design-M7).

- coach-drawer settings cog aria-label switches to the coming-soon
  tooltip copy so SR users hear the same "arrives in v1.4.21" message
  sighted users see (Design-M9).

- sources-rail static "fresh" dot becomes aria-hidden — v1.4.20 has
  no real freshness state behind it; SR users no longer hear "Fresh"
  five times in a row (Design-L4).

- summariseTitle slices on Unicode code points rather than UTF-16
  code units so a user message ending mid-emoji ("🩺") no longer
  produces a half-character on disk (Code-LOW-04).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 18:11:16 +02:00
1 parent 340239423b
commit 9f3baf940d
5 files changed
+26 -11

No files matched your search

@@ -186,7 +186,10 @@ export function CoachDrawer({
variant="ghost" variant="ghost"
size="icon" size="icon"
disabled disabled
aria-label={t("insights.coach.settings")} // Match the visible disabled state — sighted users
// see a coming-soon tooltip; SR users hear the same
// copy instead of a working "Coach settings" label.
aria-label={t("insights.coach.settingsTooltip")}
data-slot="coach-drawer-settings" data-slot="coach-drawer-settings"
className="size-8" className="size-8"
> >
@@ -98,10 +98,13 @@ export function SourcesRail({ className }: SourcesRailProps) {
<span className="text-foreground flex-1 text-xs font-medium"> <span className="text-foreground flex-1 text-xs font-medium">
{t(row.metricKey)} {t(row.metricKey)}
</span> </span>
{/* Fresh / stale indicator — v1.4.20 is a static dot. {/* Fresh / stale indicator — v1.4.20 is a static dot
v1.4.21 will reuse <IntegrationStatusPill>. */} with no real freshness state behind it (v1.4.21 plugs
in <IntegrationStatusPill>). aria-hidden so SR users
don't hear "Fresh" five times in a row for what is
actually a placeholder. */}
<span <span
aria-label={t("insights.coach.sourcesFresh")} aria-hidden="true"
className="bg-dracula-green size-1.5 rounded-full" className="bg-dracula-green size-1.5 rounded-full"
/> />
</li> </li>
+5 -1
View File
@@ -131,7 +131,11 @@ export function CorrelationCard({ result }: CorrelationCardProps) {
<Button <Button
type="button" type="button"
size="sm" size="sm"
variant="default" // Outline variant so the disabled placeholder reads as
// "this is on the roadmap" rather than "the system is
// broken right now". Flip back to default when the
// 7-day experiment feature ships in v1.5.
variant="outline"
disabled disabled
data-slot="correlation-card-cta" data-slot="correlation-card-cta"
className="gap-1.5" className="gap-1.5"
@@ -162,7 +162,7 @@ export function WeeklyReportPresentation({
> >
<span <span
data-slot="weekly-report-eyebrow" data-slot="weekly-report-eyebrow"
className="bg-dracula-purple/15 text-dracula-purple inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-medium uppercase tracking-wide" className="bg-dracula-purple/15 text-dracula-purple inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-medium print:bg-transparent print:px-0 print:text-foreground"
> >
<FileText className="h-3 w-3" aria-hidden="true" /> <FileText className="h-3 w-3" aria-hidden="true" />
{t("insights.report.eyebrow")} {t("insights.report.eyebrow")}
+10 -5
View File
@@ -34,11 +34,16 @@ const TITLE_MAX = 80;
export function summariseTitle(input: string): string { export function summariseTitle(input: string): string {
const collapsed = input.replace(/\s+/g, " ").trim(); const collapsed = input.replace(/\s+/g, " ").trim();
if (collapsed.length === 0) return "New conversation"; if (collapsed.length === 0) return "New conversation";
if (collapsed.length <= TITLE_MAX) return collapsed; // Spread to a code-point array so the slice respects multi-code-unit
// Cut at TITLE_MAX-1 chars and append a single-character ellipsis so // characters (emoji like "🩺" land as a single grapheme rather than
// the visible width matches TITLE_MAX. Cuts at the word boundary // a half "?"). The visible-length metric is grapheme count, not
// when one is within reach of the limit. // UTF-16 code units.
const sliced = collapsed.slice(0, TITLE_MAX - 1); const points = [...collapsed];
if (points.length <= TITLE_MAX) return collapsed;
// Cut at TITLE_MAX-1 code points and append a single-character
// ellipsis so the visible width matches TITLE_MAX. Cuts at the word
// boundary when one is within reach of the limit.
const sliced = points.slice(0, TITLE_MAX - 1).join("");
const lastSpace = sliced.lastIndexOf(" "); const lastSpace = sliced.lastIndexOf(" ");
const cut = lastSpace > TITLE_MAX - 20 ? sliced.slice(0, lastSpace) : sliced; const cut = lastSpace > TITLE_MAX - 20 ? sliced.slice(0, lastSpace) : sliced;
return `${cut.trimEnd()}…`; return `${cut.trimEnd()}…`;