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"
size="icon"
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"
className="size-8"
>
@@ -98,10 +98,13 @@ export function SourcesRail({ className }: SourcesRailProps) {
<span className="text-foreground flex-1 text-xs font-medium">
{t(row.metricKey)}
</span>
{/* Fresh / stale indicator — v1.4.20 is a static dot.
v1.4.21 will reuse <IntegrationStatusPill>. */}
{/* Fresh / stale indicator — v1.4.20 is a static dot
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
aria-label={t("insights.coach.sourcesFresh")}
aria-hidden="true"
className="bg-dracula-green size-1.5 rounded-full"
/>
</li>
+5 -1
View File
@@ -131,7 +131,11 @@ export function CorrelationCard({ result }: CorrelationCardProps) {
<Button
type="button"
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
data-slot="correlation-card-cta"
className="gap-1.5"
@@ -162,7 +162,7 @@ export function WeeklyReportPresentation({
>
<span
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" />
{t("insights.report.eyebrow")}
+10 -5
View File
@@ -34,11 +34,16 @@ const TITLE_MAX = 80;
export function summariseTitle(input: string): string {
const collapsed = input.replace(/\s+/g, " ").trim();
if (collapsed.length === 0) return "New conversation";
if (collapsed.length <= TITLE_MAX) return collapsed;
// Cut at TITLE_MAX-1 chars 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 = collapsed.slice(0, TITLE_MAX - 1);
// Spread to a code-point array so the slice respects multi-code-unit
// characters (emoji like "🩺" land as a single grapheme rather than
// a half "?"). The visible-length metric is grapheme count, not
// UTF-16 code units.
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 cut = lastSpace > TITLE_MAX - 20 ? sliced.slice(0, lastSpace) : sliced;
return `${cut.trimEnd()}…`;