mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
refactor(ui): retire the raw dracula utilities for semantic tokens
Migrate every remaining *-dracula-* utility site (127 lint warnings, 193 class occurrences) onto the semantic vocabulary. The swaps are pixel-identical in both themes — each target token aliases the same primitive the utility read, including the light-theme AA overrides: - green/orange/cyan/red → success/warning/info/destructive - purple → primary (same value in both themes) - pink → the new --brand-pink token (Coach gradient, Apple-Health badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg; light #a3134d — 7.32:1 card / 6.83:1 bg (computed) - yellow sites resolve to their status meaning: warn-level log icon, update-available pill and offline-geo hint → warning; the admin helpful-rate ramp → success/warning/destructive; the confidence meter mirrors the coverage-meter precedent (medium + low share the caution tint, the lit count carries the distinction) - the confidence-badge low tier dropped dead *-dracula-comment/* classes (no generated utility) for border-border + bg-muted/50 Close two rule blind spots the sweep surfaced — directional borders (border-l-dracula-red) and the parenthesised CSS-var shorthand (bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from warn to error so the class cannot regrow. var(--dracula-*) references in chart code stay: they are token references the light theme overrides, and the chart palette is pinned visually.
This commit is contained in:
1 parent
68b4dd08a7
commit
a4b204ffe4
59 files changed
+272
-214
No files matched your search
@@ -159,3 +159,36 @@ ruleTester.run("no-raw-palette-color (dracula check)", rule, {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
ruleTester.run("no-raw-palette-color (dracula blind spots)", rule, {
|
||||
valid: [
|
||||
// Semantic replacements for the directional + paren-var forms.
|
||||
{
|
||||
code: 'const c = "border-l-destructive/70 bg-success border-success";',
|
||||
filename: APP_FILE,
|
||||
options: [{ checks: ["dracula"] }],
|
||||
},
|
||||
// Token references through var() stay legal.
|
||||
{
|
||||
code: 'const c = "shadow-[0_0_0_1px_color-mix(in_oklab,var(--success)_20%,transparent)]";',
|
||||
filename: APP_FILE,
|
||||
options: [{ checks: ["dracula"] }],
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// Directional border form.
|
||||
{
|
||||
code: 'const c = "border-l-dracula-red/70";',
|
||||
filename: APP_FILE,
|
||||
options: [{ checks: ["dracula"] }],
|
||||
errors: [{ messageId: "draculaUtility" }],
|
||||
},
|
||||
// Parenthesised CSS-var utility shorthand.
|
||||
{
|
||||
code: 'const c = "bg-(--dracula-green)";',
|
||||
filename: APP_FILE,
|
||||
options: [{ checks: ["dracula"] }],
|
||||
errors: [{ messageId: "draculaUtility" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -25,9 +25,9 @@ module.exports = {
|
||||
"no-raw-palette-color": noRawPaletteColor,
|
||||
"spacing-scale": spacingScale,
|
||||
// Same module as `no-raw-palette-color`, registered under a second
|
||||
// name so the flat config can run the `dracula` check at a different
|
||||
// severity (warn) than the error-level checks. See the staged plan in
|
||||
// the rule header.
|
||||
// name so the flat config can run the `dracula` check as its own
|
||||
// named rule (now error-level too; the staged warn phase ended with
|
||||
// the semantic sweep). See the rule header.
|
||||
"no-dracula-utility": noRawPaletteColor,
|
||||
},
|
||||
};
|
||||
@@ -28,14 +28,13 @@
|
||||
* before any stylesheet loads, so it must stay a literal).
|
||||
* - `arbitrary-value` (error) — Tailwind arbitrary colour values
|
||||
* (`bg-[#…]`, `text-[oklch(…)]`). `…-[var(--token)]` never matches.
|
||||
* - `dracula` (WARN, staged) — raw `*-dracula-*` utilities. ~250
|
||||
* legacy sites predate the token system; the light-theme overrides in
|
||||
* `globals.css` defuse their AA failures, so the check ships as a
|
||||
* warning first (registered separately as
|
||||
* `healthlog/no-dracula-utility`). It moves to error once the
|
||||
* semantic sweep (status meaning → `text-success/warning/info/
|
||||
* destructive`, brand use → `--brand-*` tokens) has retired the
|
||||
* legacy sites.
|
||||
* - `dracula` (error, registered separately as
|
||||
* `healthlog/no-dracula-utility`) — raw `*-dracula-*` utilities.
|
||||
* The semantic sweep retired every legacy site: status meaning →
|
||||
* `text-success/warning/info/destructive`, purple → `primary`,
|
||||
* pink → the `--brand-pink` token. `var(--dracula-*)` references in
|
||||
* chart code are token references, not utilities, and never match —
|
||||
* the light-theme overrides in `globals.css` keep them AA.
|
||||
*
|
||||
* The `checks` option selects which detectors run, so the flat config can
|
||||
* register the same module twice at different severities (see
|
||||
@@ -78,9 +77,12 @@ const RAW_PALETTE_RE =
|
||||
|
||||
// Raw `dracula-*` utility: the pre-token palette accessed directly
|
||||
// (`text-dracula-green`, `bg-dracula-orange/15`). Same prefix set and
|
||||
// modifier tolerance as the palette check.
|
||||
// modifier tolerance as the palette check, plus the directional border
|
||||
// forms (`border-l-dracula-red`) and the parenthesised CSS-var utility
|
||||
// shorthand (`bg-(--dracula-green)`) — both bypassed the plain prefix
|
||||
// match during the staged-warning phase.
|
||||
const DRACULA_RE =
|
||||
/\b(?:text|bg|border|ring|stroke|fill|from|to|via)-dracula-[a-z]+\b/;
|
||||
/\b(?:text|bg|border(?:-[lrtbxyse])?|ring|stroke|fill|from|to|via)-(?:dracula-[a-z]+\b|\(--dracula-[a-z]+\))/;
|
||||
|
||||
// Tailwind arbitrary colour value: `bg-[#…]`, `text-[rgb(…)]`,
|
||||
// `border-[oklch(…)]`. Matching on the bracket-open plus a colour-literal
|
||||
|
||||
+7
-5
@@ -59,11 +59,13 @@ const eslintConfig = defineConfig([
|
||||
"error",
|
||||
{ checks: ["palette", "color-props", "arbitrary-value"] },
|
||||
],
|
||||
// Staged: raw `*-dracula-*` utilities warn for now — ~250 legacy
|
||||
// sites predate the token system and the light-theme overrides in
|
||||
// globals.css keep them readable. Moves to error once the semantic
|
||||
// sweep has retired the legacy sites (see the rule header).
|
||||
"healthlog/no-dracula-utility": ["warn", { checks: ["dracula"] }],
|
||||
// The semantic sweep retired every raw `*-dracula-*` utility site
|
||||
// (status meaning → success/warning/info/destructive, purple →
|
||||
// primary, pink → the --brand-pink token), so the staged warning is
|
||||
// now a hard error. `var(--dracula-*)` references in chart code are
|
||||
// token references, not utilities, and stay legal — the light-theme
|
||||
// overrides in globals.css keep them AA on both themes.
|
||||
"healthlog/no-dracula-utility": ["error", { checks: ["dracula"] }],
|
||||
// The Card primitive is gap-based; pt-/pb- on CardHeader/CardContent
|
||||
// fights the gap and re-opens the sl-001 drift class. Density lives
|
||||
// on the Card itself (`gap-2 py-3 md:py-4`). See the rule header.
|
||||
|
||||
@@ -224,7 +224,7 @@ function CoachConversationsBody() {
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
isConfirming &&
|
||||
"text-dracula-red hover:text-dracula-red",
|
||||
"text-destructive hover:text-destructive",
|
||||
)}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden="true" />
|
||||
|
||||
+28
-7
@@ -104,6 +104,17 @@
|
||||
--color-warning: var(--warning);
|
||||
--color-info: var(--info);
|
||||
|
||||
/*
|
||||
* Brand accent tokens — identity colours with no semantic (status)
|
||||
* twin. `--brand-pink` is the Coach-gradient / Apple-Health accent
|
||||
* (`from-primary to-brand-pink`, the iOS-source badge, the recording
|
||||
* mic). Per-theme values ride the `--dracula-pink` primitive, which
|
||||
* the light theme overrides to its AA-tuned counterpart:
|
||||
* dark #ff79c6 — 6.86:1 card / 5.97:1 bg; light #a3134d — 7.32:1
|
||||
* card / 6.83:1 bg (WCAG relative-luminance, computed).
|
||||
*/
|
||||
--color-brand-pink: var(--brand-pink);
|
||||
|
||||
/*
|
||||
* Status-pill text tokens. The target status pill paints coloured text
|
||||
* on a 10% same-hue tint; the raw Dracula green/orange/red would fail
|
||||
@@ -160,6 +171,14 @@
|
||||
--warning: var(--dracula-orange);
|
||||
--info: var(--dracula-cyan);
|
||||
|
||||
/*
|
||||
* Brand accent (see the @theme re-export). Aliased over the pink
|
||||
* primitive so the light theme's AA-tuned `--dracula-pink` override
|
||||
* carries through automatically — pixel-identical to the raw utility
|
||||
* sites it replaced.
|
||||
*/
|
||||
--brand-pink: var(--dracula-pink);
|
||||
|
||||
/*
|
||||
* Foreground for glyphs/labels painted on a solid `bg-success` fill. The
|
||||
* raw Dracula green (`#50fa7b`) is a bright tone, so white-on-green reads
|
||||
@@ -337,13 +356,15 @@
|
||||
--chart-5: #a34d14;
|
||||
/*
|
||||
* Light-mode (Alucard) overrides for the raw `--dracula-*` primitives.
|
||||
* ~250 legacy `text-dracula-*` / opacity-suffixed `bg-dracula-*` call
|
||||
* sites still read these directly; without an override the dark neons render on the white
|
||||
* card at 1.2–1.6:1. Each value below is tuned to the AA light palette
|
||||
* already in this block (same hue family, measured >=4.5:1 on both
|
||||
* `--card` oklch(0.988 0.003 300) and `--background` oklch(0.964 0.004
|
||||
* 300)), so every legacy utility inherits a readable tone until the
|
||||
* semantic sweep retires it.
|
||||
* The semantic sweep retired every `text-dracula-*` / `bg-dracula-*`
|
||||
* UTILITY site (lint-enforced via healthlog/no-dracula-utility), but
|
||||
* chart code still passes `var(--dracula-*)` token references
|
||||
* (Recharts stroke/fill maps) and the Coach gradient CSS below reads
|
||||
* the purple/pink primitives — without an override the dark neons
|
||||
* render on the white card at 1.2–1.6:1. Each value below is tuned to
|
||||
* the AA light palette already in this block (same hue family,
|
||||
* measured >=4.5:1 on both `--card` oklch(0.988 0.003 300) and
|
||||
* `--background` oklch(0.964 0.004 300)).
|
||||
*/
|
||||
--dracula-cyan: #036a96; /* shadows --info — 5.78:1 card / 5.39:1 bg */
|
||||
--dracula-green: #14720a; /* shadows --success — 5.89:1 card / 5.49:1 bg */
|
||||
|
||||
@@ -89,9 +89,9 @@ describe("CoachFeedbackSection", () => {
|
||||
// Two bucket rows in the table.
|
||||
const matches = (html.match(/coach-feedback-bucket/g) ?? []).length;
|
||||
expect(matches).toBe(2);
|
||||
// Helpful-rate column carries the band tint.
|
||||
expect(html).toContain("text-dracula-green"); // 80% bucket
|
||||
expect(html).toContain("text-dracula-yellow"); // 50% bucket
|
||||
// Helpful-rate column carries the band tint (semantic urgency ramp).
|
||||
expect(html).toContain("text-success"); // 80% bucket
|
||||
expect(html).toContain("text-warning"); // 50% bucket
|
||||
// Numbers visible in the markup.
|
||||
expect(html).toContain("80%");
|
||||
expect(html).toContain("50%");
|
||||
|
||||
@@ -122,7 +122,7 @@ export interface ApiTokenInfo {
|
||||
export function ConfiguredBadge() {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<Badge className="border-dracula-green/30 bg-dracula-green/15 text-dracula-green">
|
||||
<Badge className="border-success/30 bg-success/15 text-success">
|
||||
{t("admin.configured")}
|
||||
</Badge>
|
||||
);
|
||||
@@ -488,11 +488,14 @@ export function useSystemStatus() {
|
||||
}
|
||||
|
||||
/** Helpful-rate tint shared by the AI-quality + Coach-feedback tables.
|
||||
* Green ≥80 %, yellow ≥50 %, orange below. */
|
||||
* The standard urgency ramp (the medication-status-pill precedent):
|
||||
* ≥80 % success, ≥50 % warning, below destructive — a sub-50 % helpful
|
||||
* rate is a genuine quality alarm, and the percent label always carries
|
||||
* the exact value alongside the tint. */
|
||||
export function helpfulRateColour(rate: number): string {
|
||||
if (rate >= 0.8) return "text-dracula-green";
|
||||
if (rate >= 0.5) return "text-dracula-yellow";
|
||||
return "text-dracula-orange";
|
||||
if (rate >= 0.8) return "text-success";
|
||||
if (rate >= 0.5) return "text-warning";
|
||||
return "text-destructive";
|
||||
}
|
||||
|
||||
export interface PublicVersion {
|
||||
|
||||
@@ -46,10 +46,10 @@ interface FeedbackSummary {
|
||||
}
|
||||
|
||||
const SEVERITY_TINT: Record<string, string> = {
|
||||
info: "text-dracula-cyan",
|
||||
suggestion: "text-dracula-purple",
|
||||
important: "text-dracula-orange",
|
||||
urgent: "text-dracula-red",
|
||||
info: "text-info",
|
||||
suggestion: "text-primary",
|
||||
important: "text-warning",
|
||||
urgent: "text-destructive",
|
||||
};
|
||||
|
||||
export function AiQualitySection() {
|
||||
|
||||
@@ -92,9 +92,7 @@ function TokenStatusBadge({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge
|
||||
className={`bg-dracula-green/15 text-dracula-green shrink-0 ${sizeClass}`}
|
||||
>
|
||||
<Badge className={`bg-success/15 text-success shrink-0 ${sizeClass}`}>
|
||||
{t("common.active")}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -75,12 +75,12 @@ function levelIcon(level: LogLevel) {
|
||||
case "error":
|
||||
return <AlertCircle className="text-destructive h-4 w-4" />;
|
||||
case "warn":
|
||||
return <AlertTriangle className="text-dracula-yellow h-4 w-4" />;
|
||||
return <AlertTriangle className="text-warning h-4 w-4" />;
|
||||
case "debug":
|
||||
return <Info className="text-muted-foreground h-4 w-4" />;
|
||||
case "info":
|
||||
default:
|
||||
return <CheckCircle2 className="text-dracula-green h-4 w-4" />;
|
||||
return <CheckCircle2 className="text-success h-4 w-4" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ export function DangerZoneSection() {
|
||||
{wipeMsg && (
|
||||
<p
|
||||
className={`mt-3 pl-7 text-sm ${
|
||||
wipeAllData.isError ? "text-destructive" : "text-dracula-green"
|
||||
wipeAllData.isError ? "text-destructive" : "text-success"
|
||||
}`}
|
||||
>
|
||||
{wipeMsg}
|
||||
|
||||
@@ -145,20 +145,20 @@ const STATUS_STYLES: Record<InviteStatus, { dot: string; chip: string }> = {
|
||||
// Quiet tinted chips with a colour dot — the dot carries the state at
|
||||
// a glance, the tint stays low so a table of mixed states reads calm.
|
||||
active: {
|
||||
dot: "bg-dracula-green",
|
||||
chip: "border-dracula-green/30 bg-dracula-green/10 text-dracula-green",
|
||||
dot: "bg-success",
|
||||
chip: "border-success/30 bg-success/10 text-success",
|
||||
},
|
||||
expired: {
|
||||
dot: "bg-muted-foreground/60",
|
||||
chip: "border-border bg-muted/40 text-muted-foreground",
|
||||
},
|
||||
exhausted: {
|
||||
dot: "bg-dracula-cyan",
|
||||
chip: "border-dracula-cyan/30 bg-dracula-cyan/10 text-dracula-cyan",
|
||||
dot: "bg-info",
|
||||
chip: "border-info/30 bg-info/10 text-info",
|
||||
},
|
||||
revoked: {
|
||||
dot: "bg-dracula-red",
|
||||
chip: "border-dracula-red/30 bg-dracula-red/10 text-dracula-red",
|
||||
dot: "bg-destructive",
|
||||
chip: "border-destructive/30 bg-destructive/10 text-destructive",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -451,7 +451,7 @@ export function InviteTokensSection() {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-dracula-red hover:text-dracula-red"
|
||||
className="text-destructive hover:text-destructive"
|
||||
aria-label={t("admin.invites.revokeAria")}
|
||||
disabled={revoke.isPending}
|
||||
onClick={() => setRevokeTarget(invite)}
|
||||
@@ -519,7 +519,7 @@ export function InviteTokensSection() {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-dracula-red hover:text-dracula-red"
|
||||
className="text-destructive hover:text-destructive"
|
||||
disabled={revoke.isPending}
|
||||
onClick={() => setRevokeTarget(invite)}
|
||||
>
|
||||
@@ -582,7 +582,7 @@ export function InviteTokensSection() {
|
||||
className={cn(
|
||||
"min-h-11 rounded-lg border px-3 py-2 text-sm font-medium transition-colors",
|
||||
ttlDays === days
|
||||
? "border-dracula-purple/50 bg-dracula-purple/10 text-dracula-purple"
|
||||
? "border-primary/50 bg-primary/10 text-primary"
|
||||
: "border-border text-foreground hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
@@ -647,7 +647,7 @@ export function InviteTokensSection() {
|
||||
</DialogHeader>
|
||||
{/* The ticket — dashed edge marks the one-time nature. */}
|
||||
<div
|
||||
className="border-dracula-purple/40 bg-background rounded-xl border border-dashed p-4"
|
||||
className="border-primary/40 bg-background rounded-xl border border-dashed p-4"
|
||||
data-testid="admin-invites-minted"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
@@ -671,7 +671,7 @@ export function InviteTokensSection() {
|
||||
>
|
||||
{copied ? (
|
||||
<Check
|
||||
className="text-dracula-green size-4"
|
||||
className="text-success size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -464,7 +464,7 @@ export function LoginOverviewSection() {
|
||||
{isFailed ? (
|
||||
<XCircle className="text-destructive h-4 w-4" />
|
||||
) : (
|
||||
<CheckCircle2 className="text-dracula-green h-4 w-4" />
|
||||
<CheckCircle2 className="text-success h-4 w-4" />
|
||||
)}
|
||||
</td>
|
||||
<td
|
||||
@@ -503,7 +503,7 @@ export function LoginOverviewSection() {
|
||||
</span>
|
||||
{copiedIp === entry.id ? (
|
||||
<Check
|
||||
className="text-dracula-green h-3 w-3 shrink-0"
|
||||
className="text-success h-3 w-3 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -115,7 +115,7 @@ export function RecentAuditPreview() {
|
||||
{isFailed ? (
|
||||
<XCircle className="text-destructive h-4 w-4" />
|
||||
) : (
|
||||
<CheckCircle2 className="text-dracula-green h-4 w-4" />
|
||||
<CheckCircle2 className="text-success h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
|
||||
@@ -70,7 +70,7 @@ export function SystemStatusSection() {
|
||||
}
|
||||
className={
|
||||
status.database === "connected"
|
||||
? "text-dracula-green"
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
}
|
||||
/>
|
||||
@@ -108,9 +108,7 @@ export function SystemStatusSection() {
|
||||
: t("admin.workerStopped")
|
||||
}
|
||||
className={
|
||||
status.worker.running
|
||||
? "text-dracula-green"
|
||||
: "text-destructive"
|
||||
status.worker.running ? "text-success" : "text-destructive"
|
||||
}
|
||||
/>
|
||||
{status.worker.lastReminderCheck && (
|
||||
@@ -131,7 +129,7 @@ export function SystemStatusSection() {
|
||||
}
|
||||
className={
|
||||
status.integrations.umami.enabled
|
||||
? "text-dracula-green"
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
}
|
||||
/>
|
||||
@@ -147,7 +145,7 @@ export function SystemStatusSection() {
|
||||
}
|
||||
className={
|
||||
status.integrations.glitchtip.enabled
|
||||
? "text-dracula-green"
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
}
|
||||
/>
|
||||
@@ -157,7 +155,7 @@ export function SystemStatusSection() {
|
||||
icon={BellRing}
|
||||
label={t("admin.integrationWebPush")}
|
||||
value={t("admin.configured")}
|
||||
className="text-dracula-green"
|
||||
className="text-success"
|
||||
/>
|
||||
)}
|
||||
{/* v1.4.27 R5 — offline GeoLite2 availability. Renders only
|
||||
@@ -175,9 +173,7 @@ export function SystemStatusSection() {
|
||||
})
|
||||
}
|
||||
className={
|
||||
version.offlineGeoEnabled
|
||||
? "text-dracula-green"
|
||||
: "text-dracula-yellow"
|
||||
version.offlineGeoEnabled ? "text-success" : "text-warning"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -61,7 +61,7 @@ export function SystemStatusSummary() {
|
||||
}
|
||||
className={
|
||||
status.database === "connected"
|
||||
? "text-dracula-green"
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
}
|
||||
/>
|
||||
@@ -74,7 +74,7 @@ export function SystemStatusSummary() {
|
||||
: t("admin.workerStopped")
|
||||
}
|
||||
className={
|
||||
status.worker.running ? "text-dracula-green" : "text-destructive"
|
||||
status.worker.running ? "text-success" : "text-destructive"
|
||||
}
|
||||
/>
|
||||
<StatusItem
|
||||
@@ -112,9 +112,7 @@ export function SystemStatusSummary() {
|
||||
})
|
||||
}
|
||||
className={
|
||||
version.offlineGeoEnabled
|
||||
? "text-dracula-green"
|
||||
: "text-dracula-yellow"
|
||||
version.offlineGeoEnabled ? "text-success" : "text-warning"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -190,7 +190,7 @@ export function UserManagementSection() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={`min-h-11 min-w-11 px-3 text-xs ${u.mfaEnforced ? "text-dracula-green" : ""}`}
|
||||
className={`min-h-11 min-w-11 px-3 text-xs ${u.mfaEnforced ? "text-success" : ""}`}
|
||||
onClick={() =>
|
||||
updateUser.mutate({
|
||||
id: u.id,
|
||||
@@ -554,7 +554,7 @@ export function UserManagementSection() {
|
||||
</div>
|
||||
{resetMsg && (
|
||||
<p
|
||||
className={`text-sm ${resetMsg === t("admin.passwordReset") ? "text-dracula-green" : "text-destructive"}`}
|
||||
className={`text-sm ${resetMsg === t("admin.passwordReset") ? "text-success" : "text-destructive"}`}
|
||||
>
|
||||
{resetMsg}
|
||||
</p>
|
||||
|
||||
@@ -97,7 +97,7 @@ export function VersionTileSection() {
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-dracula-yellow/15 text-dracula-yellow border-dracula-yellow/30 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-opacity hover:opacity-80"
|
||||
className="bg-warning/15 text-warning border-warning/30 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-opacity hover:opacity-80"
|
||||
>
|
||||
<ArrowUpCircle className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("admin.overview.versionTileUpdateAvailable", {
|
||||
@@ -106,7 +106,7 @@ export function VersionTileSection() {
|
||||
})}
|
||||
</a>
|
||||
) : upToDate ? (
|
||||
<span className="text-dracula-green inline-flex items-center gap-1.5 text-xs font-medium">
|
||||
<span className="text-success inline-flex items-center gap-1.5 text-xs font-medium">
|
||||
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("admin.overview.versionTileUpToDate")}
|
||||
</span>
|
||||
|
||||
@@ -315,9 +315,7 @@ export function MedicationComplianceChart({
|
||||
// mood tiles.
|
||||
const trendColor = ((): string => {
|
||||
if (!trend || trend.direction === "stable") return "text-muted-foreground";
|
||||
return trend.direction === "up"
|
||||
? "text-dracula-green"
|
||||
: "text-dracula-orange";
|
||||
return trend.direction === "up" ? "text-success" : "text-warning";
|
||||
})();
|
||||
|
||||
const TrendIcon = !trend
|
||||
|
||||
@@ -62,22 +62,24 @@ describe("<ConfidenceMeter> bars variant (default)", () => {
|
||||
expect(lit).toBe(2);
|
||||
});
|
||||
|
||||
it("uses green band class at value>=80", () => {
|
||||
it("uses the success tint at value>=80", () => {
|
||||
const html = render(<ConfidenceMeter value={85} />);
|
||||
expect(html).toContain('data-confidence-band="high"');
|
||||
expect(html).toMatch(/dracula-green/);
|
||||
expect(html).toMatch(/bg-success/);
|
||||
});
|
||||
|
||||
it("uses yellow band class at value 50..79", () => {
|
||||
it("uses the caution tint at value 50..79", () => {
|
||||
const html = render(<ConfidenceMeter value={65} />);
|
||||
expect(html).toContain('data-confidence-band="medium"');
|
||||
expect(html).toMatch(/dracula-yellow/);
|
||||
expect(html).toMatch(/bg-warning/);
|
||||
});
|
||||
|
||||
it("uses orange band class at value 25..49", () => {
|
||||
// Medium and low share the caution tint (coverage-meter precedent);
|
||||
// the band attribute + lit-bar count carry the distinction.
|
||||
it("uses the caution tint at value 25..49", () => {
|
||||
const html = render(<ConfidenceMeter value={35} />);
|
||||
expect(html).toContain('data-confidence-band="low"');
|
||||
expect(html).toMatch(/dracula-orange/);
|
||||
expect(html).toMatch(/bg-warning/);
|
||||
});
|
||||
|
||||
it("renders the 'draft' pill INSTEAD of bars when value<25", () => {
|
||||
|
||||
@@ -148,13 +148,13 @@ describe("<RecommendationsGrid>", () => {
|
||||
rec("d", "info", "info-rec"),
|
||||
];
|
||||
const html = render(<RecommendationsGrid recs={recs} />);
|
||||
// Each card gets a Dracula-token border-l class. The exact class
|
||||
// Each card gets a semantic-token border-l class. The exact class
|
||||
// varies by severity; we assert at least one of each is in the
|
||||
// output.
|
||||
expect(html).toMatch(/border-l-dracula-red/);
|
||||
expect(html).toMatch(/border-l-dracula-orange/);
|
||||
expect(html).toMatch(/border-l-dracula-purple/);
|
||||
expect(html).toMatch(/border-l-dracula-cyan/);
|
||||
expect(html).toMatch(/border-l-destructive/);
|
||||
expect(html).toMatch(/border-l-warning/);
|
||||
expect(html).toMatch(/border-l-primary/);
|
||||
expect(html).toMatch(/border-l-info/);
|
||||
});
|
||||
|
||||
it("renders nothing when recs is empty", () => {
|
||||
|
||||
@@ -68,9 +68,9 @@ describe("<SuggestedPrompts>", () => {
|
||||
expect(matches.length).toBe(2);
|
||||
});
|
||||
|
||||
it("uses Dracula purple chip styling tokens", () => {
|
||||
it("uses the primary-token chip styling", () => {
|
||||
const html = render(<SuggestedPrompts onPick={() => {}} />);
|
||||
expect(html).toMatch(/border-dracula-purple\/18/);
|
||||
expect(html).toMatch(/border-primary\/18/);
|
||||
});
|
||||
|
||||
it("invokes onPick with the localised prompt string when a chip is clicked", () => {
|
||||
|
||||
@@ -726,7 +726,7 @@ export function CoachConversation({
|
||||
) : null}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink flex size-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br shadow-sm"
|
||||
className="from-primary to-brand-pink flex size-9 shrink-0 items-center justify-center rounded-full bg-gradient-to-br shadow-sm"
|
||||
>
|
||||
<Sparkles className="text-background size-4" />
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export function CoachHero({ composer, scopeHint }: CoachHeroProps) {
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 -z-10",
|
||||
"bg-[radial-gradient(60%_50%_at_50%_38%,color-mix(in_srgb,var(--dracula-purple)_14%,transparent),transparent_70%)]",
|
||||
"bg-[radial-gradient(60%_50%_at_50%_38%,color-mix(in_srgb,var(--primary)_14%,transparent),transparent_70%)]",
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function CoachHero({ composer, scopeHint }: CoachHeroProps) {
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"from-dracula-purple to-dracula-pink flex size-12 items-center justify-center rounded-2xl bg-gradient-to-br shadow-sm",
|
||||
"from-primary to-brand-pink flex size-12 items-center justify-center rounded-2xl bg-gradient-to-br shadow-sm",
|
||||
"motion-safe:animate-in motion-safe:fade-in-0 motion-safe:zoom-in-95 motion-safe:duration-500",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -428,7 +428,7 @@ export function CoachInput({
|
||||
className={cn(
|
||||
"size-11 shrink-0 rounded-xl transition-colors sm:size-9",
|
||||
listening
|
||||
? "text-dracula-pink bg-dracula-pink/10 hover:text-dracula-pink"
|
||||
? "text-brand-pink bg-brand-pink/10 hover:text-brand-pink"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -553,7 +553,7 @@ export function CoachInput({
|
||||
className={cn(
|
||||
"border-border/60 bg-muted/40 group rounded-2xl border",
|
||||
"shadow-sm transition-colors",
|
||||
"focus-within:border-dracula-purple/50 focus-within:ring-dracula-purple/15 focus-within:bg-background focus-within:ring-2",
|
||||
"focus-within:border-primary/50 focus-within:ring-primary/15 focus-within:bg-background focus-within:ring-2",
|
||||
// Both surfaces share ONE baseline row: the drawer flanks the
|
||||
// textarea with mic (left) + send (right); the page leads with a
|
||||
// `+` actions menu, then the textarea, then mic + send. `items-end`
|
||||
@@ -601,7 +601,7 @@ export function CoachInput({
|
||||
// v1.18.7 — calm, thin scrollbar inside the composer when
|
||||
// dictation overruns 6 lines (see also the thread/history
|
||||
// scroll regions). Scoped here, not in globals.css.
|
||||
"[scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--dracula-purple)_35%,transparent)_transparent]",
|
||||
"[scrollbar-width:thin] [scrollbar-color:color-mix(in_srgb,var(--primary)_35%,transparent)_transparent]",
|
||||
"placeholder:text-muted-foreground disabled:opacity-60",
|
||||
// Keep the placeholder a single line on narrow phones: a long
|
||||
// hint used to wrap to two lines inside the one-row composer and
|
||||
|
||||
@@ -37,7 +37,7 @@ import { TypingDots } from "./message-thread";
|
||||
/** Shared quiet text-button styling for the per-question actions. */
|
||||
const guidedActionClass = cn(
|
||||
"text-muted-foreground hover:text-foreground inline-flex min-h-8 items-center rounded-full px-2.5 text-xs",
|
||||
"hover:bg-dracula-purple/10 focus-visible:ring-dracula-purple/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"hover:bg-primary/10 focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"disabled:opacity-60",
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ function GuidedAvatar({ icon: Icon }: { icon: typeof ClipboardCheck }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-brand-pink mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
<Icon className="text-background size-3.5" />
|
||||
</div>
|
||||
@@ -66,9 +66,8 @@ function ProgressDots({ current, total }: { current: number; total: number }) {
|
||||
key={i}
|
||||
className={cn(
|
||||
"size-1.5 rounded-full",
|
||||
i + 1 < current && "bg-dracula-purple",
|
||||
i + 1 === current &&
|
||||
"ring-dracula-purple bg-dracula-purple/40 ring-1",
|
||||
i + 1 < current && "bg-primary",
|
||||
i + 1 === current && "ring-primary bg-primary/40 ring-1",
|
||||
i + 1 > current && "bg-muted-foreground/25",
|
||||
)}
|
||||
/>
|
||||
@@ -125,7 +124,7 @@ export function GuidedQuestionBubble({
|
||||
<div className="flex max-w-[calc(80%-2.625rem)] min-w-0 flex-col gap-1.5">
|
||||
<div
|
||||
className={cn(
|
||||
"border-dracula-purple/30 bg-dracula-purple/10 text-foreground",
|
||||
"border-primary/30 bg-primary/10 text-foreground",
|
||||
"rounded-xl rounded-tl-sm border px-3.5 py-2.5",
|
||||
"text-sm leading-relaxed",
|
||||
)}
|
||||
@@ -134,7 +133,7 @@ export function GuidedQuestionBubble({
|
||||
<>
|
||||
<p
|
||||
data-slot="coach-guided-progress"
|
||||
className="text-dracula-purple mb-1 flex items-center gap-2 text-[10px] font-semibold tracking-wider uppercase"
|
||||
className="text-primary mb-1 flex items-center gap-2 text-[10px] font-semibold tracking-wider uppercase"
|
||||
>
|
||||
{t("insights.coach.guided.progress", {
|
||||
current: progress.current,
|
||||
@@ -208,7 +207,7 @@ export function GuidedSummaryBubble({
|
||||
<GuidedAvatar icon={ClipboardCheck} />
|
||||
<div
|
||||
className={cn(
|
||||
"border-dracula-purple/30 bg-dracula-purple/10 text-foreground",
|
||||
"border-primary/30 bg-primary/10 text-foreground",
|
||||
"max-w-[calc(80%-2.625rem)] min-w-0 rounded-xl rounded-tl-sm border px-3.5 py-2.5",
|
||||
)}
|
||||
>
|
||||
@@ -226,8 +225,8 @@ export function GuidedSummaryBubble({
|
||||
href="/settings/ai"
|
||||
data-slot="coach-guided-summary-link"
|
||||
className={cn(
|
||||
"text-dracula-purple mt-2 inline-flex min-h-8 items-center gap-1 text-xs font-medium",
|
||||
"focus-visible:ring-dracula-purple/40 rounded hover:underline focus-visible:ring-2 focus-visible:outline-none",
|
||||
"text-primary mt-2 inline-flex min-h-8 items-center gap-1 text-xs font-medium",
|
||||
"focus-visible:ring-primary/40 rounded hover:underline focus-visible:ring-2 focus-visible:outline-none",
|
||||
)}
|
||||
>
|
||||
{t("insights.coach.guided.summaryLink")}
|
||||
|
||||
@@ -44,14 +44,14 @@ export function GuidedQuestionsCard({
|
||||
<div
|
||||
data-slot="coach-guided-offer"
|
||||
className={cn(
|
||||
"border-dracula-purple/30 from-dracula-purple/10 to-dracula-pink/5",
|
||||
"border-primary/30 from-primary/10 to-brand-pink/5",
|
||||
"mb-2 rounded-lg border bg-gradient-to-r px-3 py-2.5",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-brand-pink mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
<MessageCircleQuestion className="text-background size-3.5" />
|
||||
</div>
|
||||
@@ -71,8 +71,8 @@ export function GuidedQuestionsCard({
|
||||
disabled={disabled}
|
||||
onClick={onStart}
|
||||
className={cn(
|
||||
"bg-dracula-purple/15 text-dracula-purple inline-flex min-h-8 items-center rounded-full px-3 text-xs font-medium",
|
||||
"hover:bg-dracula-purple/25 focus-visible:ring-dracula-purple/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"bg-primary/15 text-primary inline-flex min-h-8 items-center rounded-full px-3 text-xs font-medium",
|
||||
"hover:bg-primary/25 focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
@@ -85,7 +85,7 @@ export function GuidedQuestionsCard({
|
||||
onClick={onLater}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground inline-flex min-h-8 items-center rounded-full px-2.5 text-xs",
|
||||
"hover:bg-dracula-purple/10 focus-visible:ring-dracula-purple/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"hover:bg-primary/10 focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
@@ -98,7 +98,7 @@ export function GuidedQuestionsCard({
|
||||
onClick={onDismissAll}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground inline-flex min-h-8 items-center rounded-full px-2.5 text-xs",
|
||||
"hover:bg-dracula-purple/10 focus-visible:ring-dracula-purple/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"hover:bg-primary/10 focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -151,7 +151,7 @@ export function HistoryRail({
|
||||
"group flex items-center gap-1.5 rounded-lg px-2.5 py-2",
|
||||
"text-sm transition-colors",
|
||||
isActive
|
||||
? "bg-dracula-purple/15 text-foreground"
|
||||
? "bg-primary/15 text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted/40 hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -186,7 +186,7 @@ export function HistoryRail({
|
||||
data-confirming={isConfirming ? "true" : undefined}
|
||||
className={cn(
|
||||
"size-11 shrink-0",
|
||||
isConfirming && "text-dracula-red hover:text-dracula-red",
|
||||
isConfirming && "text-destructive hover:text-destructive",
|
||||
)}
|
||||
>
|
||||
<Trash2 className="size-4" aria-hidden="true" />
|
||||
|
||||
@@ -181,14 +181,14 @@ export function placeInterleaved(
|
||||
* is mixed down so the bar reads as a hairline accent, not a hard edge.
|
||||
*/
|
||||
export const COACH_SCROLLBAR = cn(
|
||||
"[scrollbar-color:color-mix(in_srgb,var(--dracula-purple)_30%,transparent)_transparent] [scrollbar-width:thin]",
|
||||
"[scrollbar-color:color-mix(in_srgb,var(--primary)_30%,transparent)_transparent] [scrollbar-width:thin]",
|
||||
"[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2",
|
||||
"[&::-webkit-scrollbar-track]:bg-transparent",
|
||||
"[&::-webkit-scrollbar-button]:hidden [&::-webkit-scrollbar-button]:size-0",
|
||||
"[&::-webkit-scrollbar-thumb]:rounded-full",
|
||||
"[&::-webkit-scrollbar-thumb]:border-[3px] [&::-webkit-scrollbar-thumb]:border-transparent [&::-webkit-scrollbar-thumb]:bg-clip-content",
|
||||
"[&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--dracula-purple)_30%,transparent)]",
|
||||
"hover:[&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--dracula-purple)_45%,transparent)]",
|
||||
"[&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--primary)_30%,transparent)]",
|
||||
"hover:[&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--primary)_45%,transparent)]",
|
||||
);
|
||||
|
||||
function isPinnedToBottom(el: HTMLElement, slack = 64): boolean {
|
||||
@@ -660,7 +660,7 @@ function CopyMessageButton({
|
||||
className={COACH_ICON_BUTTON}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="text-dracula-green size-3.5" aria-hidden="true" />
|
||||
<Check className="text-success size-3.5" aria-hidden="true" />
|
||||
) : (
|
||||
<Copy className="size-3.5" aria-hidden="true" />
|
||||
)}
|
||||
@@ -897,7 +897,7 @@ export function MessageThread({
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink flex size-12 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-brand-pink flex size-12 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
<Sparkles className="text-background size-5" />
|
||||
</div>
|
||||
@@ -1257,7 +1257,7 @@ function ChatBubble({
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-brand-pink mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
{providerType === "refusal" ? (
|
||||
<Bot className="text-background size-3.5" />
|
||||
@@ -1545,7 +1545,7 @@ function RememberUserMessage({ content }: { content: string }) {
|
||||
data-slot="coach-remember-message-done"
|
||||
className="text-muted-foreground flex items-center gap-1 text-xs outline-none"
|
||||
>
|
||||
<Check className="text-dracula-green size-3" aria-hidden="true" />
|
||||
<Check className="text-success size-3" aria-hidden="true" />
|
||||
{t(
|
||||
settled === "adopted"
|
||||
? "insights.coach.rememberMessage.done"
|
||||
@@ -1616,7 +1616,7 @@ export function TypingDots({ label }: { label: string }) {
|
||||
{[0, 1, 2].map((i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="bg-dracula-purple/70 size-1.5 animate-bounce rounded-full motion-reduce:animate-none"
|
||||
className="bg-primary/70 size-1.5 animate-bounce rounded-full motion-reduce:animate-none"
|
||||
style={{ animationDelay: `${i * 150}ms`, animationDuration: "1s" }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function ReminderSuggestionCard({
|
||||
data-slot="coach-reminder-suggestion-done"
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-xs"
|
||||
>
|
||||
<Check className="text-dracula-green size-3.5" aria-hidden="true" />
|
||||
<Check className="text-success size-3.5" aria-hidden="true" />
|
||||
{t(
|
||||
settled === "accepted"
|
||||
? "coach.reminderSuggestion.accepted"
|
||||
@@ -89,7 +89,7 @@ export function ReminderSuggestionCard({
|
||||
<div className="flex items-start gap-2">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-brand-pink mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
<BellPlus className="text-background size-3.5" />
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@ export function ReminderSuggestionCard({
|
||||
onClick={() => act.mutate("accept")}
|
||||
disabled={busy}
|
||||
className={cn(
|
||||
"bg-dracula-purple/90 text-background hover:bg-dracula-purple",
|
||||
"bg-primary/90 text-background hover:bg-primary",
|
||||
"focus-visible:ring-ring/50 inline-flex min-h-9 items-center gap-1.5",
|
||||
"rounded-md px-3 py-1.5 text-xs font-medium outline-none",
|
||||
"focus-visible:ring-2 disabled:opacity-50",
|
||||
|
||||
@@ -83,10 +83,7 @@ export function ScopeHintBadge({
|
||||
"text-xs font-medium",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="text-dracula-purple size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon className="text-primary size-3.5 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">{prefix}</span>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export function SelfContextAdoptOffer({
|
||||
<div
|
||||
data-slot="coach-self-context-adopt"
|
||||
className={cn(
|
||||
"border-dracula-purple/30 bg-dracula-purple/10 mb-2 flex items-center gap-2 rounded-lg border px-3 py-2",
|
||||
"border-primary/30 bg-primary/10 mb-2 flex items-center gap-2 rounded-lg border px-3 py-2",
|
||||
)}
|
||||
>
|
||||
{settled ? (
|
||||
@@ -85,7 +85,7 @@ export function SelfContextAdoptOffer({
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-xs"
|
||||
>
|
||||
<Check
|
||||
className="text-dracula-green size-3.5 shrink-0"
|
||||
className="text-success size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t(
|
||||
@@ -97,7 +97,7 @@ export function SelfContextAdoptOffer({
|
||||
) : (
|
||||
<>
|
||||
<BookmarkPlus
|
||||
className="text-dracula-purple size-3.5 shrink-0"
|
||||
className="text-primary size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-muted-foreground min-w-0 flex-1 text-xs">
|
||||
@@ -109,8 +109,8 @@ export function SelfContextAdoptOffer({
|
||||
disabled={adopt.isPending}
|
||||
onClick={() => adopt.mutate()}
|
||||
className={cn(
|
||||
"text-dracula-purple inline-flex min-h-8 shrink-0 items-center gap-1 rounded-full px-2.5 text-xs font-medium",
|
||||
"hover:bg-dracula-purple/15 focus-visible:ring-dracula-purple/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"text-primary inline-flex min-h-8 shrink-0 items-center gap-1 rounded-full px-2.5 text-xs font-medium",
|
||||
"hover:bg-primary/15 focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
@@ -132,7 +132,7 @@ export function SelfContextAdoptOffer({
|
||||
}}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground flex min-h-8 items-center rounded-full px-1.5",
|
||||
"hover:bg-dracula-purple/15 focus-visible:ring-dracula-purple/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"hover:bg-primary/15 focus-visible:ring-primary/40 focus-visible:ring-2 focus-visible:outline-none",
|
||||
)}
|
||||
>
|
||||
<X className="size-3" aria-hidden="true" />
|
||||
|
||||
@@ -86,7 +86,7 @@ export function SourceChips({ provenance, className }: SourceChipsProps) {
|
||||
data-slot="coach-source-chip"
|
||||
data-metric={chip.metric}
|
||||
className={cn(
|
||||
"border-dracula-cyan/25 text-dracula-cyan/90",
|
||||
"border-info/25 text-info/90",
|
||||
"inline-flex items-center gap-1 rounded-full border bg-transparent",
|
||||
"px-2 py-0.5 text-[11px] leading-none",
|
||||
)}
|
||||
|
||||
@@ -139,10 +139,10 @@ export function SourcesRail({ className, activeScopeLabel }: SourcesRailProps) {
|
||||
{activeScopeLabel ? (
|
||||
<div
|
||||
data-slot="coach-sources-active-scope"
|
||||
className="border-dracula-purple/30 bg-dracula-purple/5 text-foreground flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium"
|
||||
className="border-primary/30 bg-primary/5 text-foreground flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-medium"
|
||||
>
|
||||
<Target
|
||||
className="text-dracula-purple size-3.5 shrink-0"
|
||||
className="text-primary size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="min-w-0 truncate">
|
||||
|
||||
@@ -50,7 +50,7 @@ export function SuggestedActionCard({
|
||||
data-slot="coach-suggested-action-done"
|
||||
className="text-muted-foreground flex items-center gap-1.5 text-xs"
|
||||
>
|
||||
<Check className="text-dracula-green size-3.5" aria-hidden="true" />
|
||||
<Check className="text-success size-3.5" aria-hidden="true" />
|
||||
{t(
|
||||
settled === "applied"
|
||||
? "coach.suggestedAction.applied"
|
||||
@@ -77,7 +77,7 @@ export function SuggestedActionCard({
|
||||
<div className="flex items-start gap-2">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-cyan mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-info mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
<Sparkles className="text-background size-3.5" />
|
||||
</div>
|
||||
@@ -102,7 +102,7 @@ export function SuggestedActionCard({
|
||||
onClick={() => apply.mutate()}
|
||||
disabled={busy}
|
||||
className={cn(
|
||||
"bg-dracula-purple/90 text-background hover:bg-dracula-purple",
|
||||
"bg-primary/90 text-background hover:bg-primary",
|
||||
"focus-visible:ring-ring/50 inline-flex min-h-11 items-center gap-1.5 sm:min-h-9",
|
||||
"rounded-md px-3 py-1.5 text-xs font-medium outline-none",
|
||||
"focus-visible:ring-2 disabled:opacity-50",
|
||||
|
||||
@@ -12,5 +12,9 @@ export type ConfidenceBand = "low" | "moderate" | "high";
|
||||
export const CONFIDENCE_BADGE_CLASS: Record<ConfidenceBand, string> = {
|
||||
high: "border-success/40 bg-success/10 text-success",
|
||||
moderate: "border-warning/40 bg-warning/10 text-warning",
|
||||
low: "border-dracula-comment/40 bg-dracula-comment/10 text-muted-foreground",
|
||||
// Neutral (non-status) tier. The former `*-dracula-comment/*` classes
|
||||
// had no generated utility (`--color-dracula-comment` was never in the
|
||||
// @theme block) and silently painted nothing; border/muted is the real
|
||||
// neutral vocabulary.
|
||||
low: "border-border bg-muted/50 text-muted-foreground",
|
||||
};
|
||||
@@ -47,22 +47,26 @@ function litBarsFor(value: number): number {
|
||||
return lit;
|
||||
}
|
||||
|
||||
// Semantic severities only (the coverage-meter precedent): medium and
|
||||
// low both ride the caution `--warning` — the lit-bar count and the
|
||||
// numeric aria-label carry the medium↔low distinction, never colour
|
||||
// alone.
|
||||
const BAR_LIT_COLOUR_BY_BAND: Record<
|
||||
Exclude<ConfidenceBand, "draft">,
|
||||
string
|
||||
> = {
|
||||
high: "bg-dracula-green",
|
||||
medium: "bg-dracula-yellow",
|
||||
low: "bg-dracula-orange",
|
||||
high: "bg-success",
|
||||
medium: "bg-warning",
|
||||
low: "bg-warning",
|
||||
};
|
||||
|
||||
const RING_STROKE_COLOUR_BY_BAND: Record<
|
||||
Exclude<ConfidenceBand, "draft">,
|
||||
string
|
||||
> = {
|
||||
high: "stroke-dracula-green",
|
||||
medium: "stroke-dracula-yellow",
|
||||
low: "stroke-dracula-orange",
|
||||
high: "stroke-success",
|
||||
medium: "stroke-warning",
|
||||
low: "stroke-warning",
|
||||
};
|
||||
|
||||
function DraftPill({ ariaLabel }: { ariaLabel: string }) {
|
||||
@@ -72,7 +76,7 @@ function DraftPill({ ariaLabel }: { ariaLabel: string }) {
|
||||
data-confidence-band="draft"
|
||||
role="img"
|
||||
aria-label={ariaLabel}
|
||||
className="bg-dracula-red/10 text-dracula-red border-dracula-red/25 inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase"
|
||||
className="bg-destructive/10 text-destructive border-destructive/25 inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium tracking-wide uppercase"
|
||||
>
|
||||
{t("insights.recommendation.confidenceDraft")}
|
||||
</span>
|
||||
|
||||
@@ -51,9 +51,9 @@ interface CorrelationCardProps {
|
||||
}
|
||||
|
||||
const TONE_BAR_CLASSNAME: Record<CorrelationResult["kind"], string> = {
|
||||
"bp-compliance": "bg-dracula-pink",
|
||||
"mood-pulse": "bg-dracula-cyan",
|
||||
"weight-weekday": "bg-dracula-purple",
|
||||
"bp-compliance": "bg-brand-pink",
|
||||
"mood-pulse": "bg-info",
|
||||
"weight-weekday": "bg-primary",
|
||||
};
|
||||
|
||||
const TITLE_KEY: Record<CorrelationResult["kind"], string> = {
|
||||
@@ -208,12 +208,13 @@ export function CorrelationCard({ result }: CorrelationCardProps) {
|
||||
}
|
||||
|
||||
function tonesByKind(kind: CorrelationResult["kind"]): string {
|
||||
// Keep in step with TONE_BAR_CLASSNAME above — same token per kind.
|
||||
switch (kind) {
|
||||
case "bp-compliance":
|
||||
return "var(--dracula-pink)";
|
||||
return "var(--brand-pink)";
|
||||
case "mood-pulse":
|
||||
return "var(--dracula-cyan)";
|
||||
return "var(--info)";
|
||||
case "weight-weekday":
|
||||
return "var(--dracula-purple)";
|
||||
return "var(--primary)";
|
||||
}
|
||||
}
|
||||
@@ -285,7 +285,7 @@ export function HeroStrip({
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles
|
||||
className="text-dracula-purple h-5 w-5 shrink-0"
|
||||
className="text-primary h-5 w-5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<h1
|
||||
|
||||
@@ -252,8 +252,8 @@ export function LayoutCoachFab() {
|
||||
// Dark glyph on the purple/pink gradient — white sat at
|
||||
// ≈2.3:1 against the gradient midpoint; the background token
|
||||
// reads ≈6.5:1.
|
||||
"from-dracula-purple to-dracula-pink text-background bg-gradient-to-br",
|
||||
"hover:from-dracula-purple/90 hover:to-dracula-pink/90",
|
||||
"from-primary to-brand-pink text-background bg-gradient-to-br",
|
||||
"hover:from-primary/90 hover:to-brand-pink/90",
|
||||
// The default ring alone is hard to see against the gradient;
|
||||
// the offset ring draws a clear halo around the circle.
|
||||
"focus-visible:ring-offset-background focus-visible:ring-offset-2",
|
||||
@@ -285,7 +285,7 @@ export function LayoutCoachFab() {
|
||||
<span
|
||||
data-slot="coach-fab-unread"
|
||||
aria-hidden="true"
|
||||
className="border-background bg-dracula-cyan absolute top-0.5 right-0.5 size-3 rounded-full border-2"
|
||||
className="border-background bg-info absolute top-0.5 right-0.5 size-3 rounded-full border-2"
|
||||
/>
|
||||
) : null}
|
||||
</Button>
|
||||
|
||||
@@ -102,7 +102,7 @@ export function MeasurementDiversityNudge({
|
||||
type="button"
|
||||
data-slot="measurement-diversity-nudge"
|
||||
aria-label={triggerLabel}
|
||||
className="text-dracula-yellow hover:text-dracula-yellow/80 focus-visible:ring-ring/50 -mx-2 -my-3 inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
||||
className="text-warning hover:text-warning/80 focus-visible:ring-ring/50 -mx-2 -my-3 inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<Lightbulb className="size-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
@@ -118,8 +118,8 @@ export function PersonalRecordBadge({
|
||||
// `--success` resolves to the AA-tuned green per theme: a deep
|
||||
// forest green on the light card (#14720a) and Dracula's neon
|
||||
// green on the dark card (#50fa7b). Both clear 4.5:1 against
|
||||
// their card background — the previous `text-dracula-green` on
|
||||
// `bg-dracula-green/10` measured ~2.6:1 in dark mode.
|
||||
// their card background — the previous `text-success` on
|
||||
// `bg-success/10` measured ~2.6:1 in dark mode.
|
||||
"border-success/40 bg-success/15 text-success inline-flex shrink-0 items-center rounded-full border px-1.5 py-0.5 text-[10px] leading-none font-semibold tracking-wide uppercase tabular-nums",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -62,10 +62,10 @@ export function sortRecommendationsBySeverity(
|
||||
}
|
||||
|
||||
const SEVERITY_BORDER_CLASSES: Record<string, string> = {
|
||||
urgent: "border-l-dracula-red/70",
|
||||
important: "border-l-dracula-orange/70",
|
||||
suggestion: "border-l-dracula-purple/70",
|
||||
info: "border-l-dracula-cyan/70",
|
||||
urgent: "border-l-destructive/70",
|
||||
important: "border-l-warning/70",
|
||||
suggestion: "border-l-primary/70",
|
||||
info: "border-l-info/70",
|
||||
};
|
||||
|
||||
const SEVERITY_BORDER_FALLBACK = "border-l-border";
|
||||
|
||||
@@ -91,11 +91,11 @@ export function SuggestedPrompts({
|
||||
// target clears the 44 px iOS/WCAG floor on phones (`min-h-11`)
|
||||
// and relaxes to the denser 36 px from `sm` up.
|
||||
className={cn(
|
||||
"border-dracula-purple/18 hover:border-dracula-purple/40 hover:text-foreground",
|
||||
"border-primary/18 hover:border-primary/40 hover:text-foreground",
|
||||
"text-muted-foreground inline-flex min-h-11 items-center gap-1.5 sm:min-h-9",
|
||||
"rounded-full border bg-transparent px-3 py-1.5 text-xs",
|
||||
"transition-colors focus-visible:ring-2 focus-visible:outline-none",
|
||||
"focus-visible:ring-dracula-purple/50",
|
||||
"focus-visible:ring-primary/50",
|
||||
)}
|
||||
>
|
||||
<Quote className="h-3 w-3 shrink-0" aria-hidden="true" />
|
||||
|
||||
@@ -86,7 +86,7 @@ export function TrendCaptionCard({
|
||||
className="border-border/60 bg-card/40 flex items-start gap-2 rounded-md border p-3"
|
||||
>
|
||||
<Sparkles
|
||||
className="text-dracula-purple mt-0.5 h-3.5 w-3.5 shrink-0"
|
||||
className="text-primary mt-0.5 h-3.5 w-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
|
||||
@@ -70,15 +70,15 @@ describe("MeasurementList — APPLE_HEALTH badge", () => {
|
||||
expect(html).not.toContain(">APPLE_HEALTH<");
|
||||
});
|
||||
|
||||
it("paints the badge with the Dracula-pink utility classes", () => {
|
||||
it("paints the badge with the brand-pink utility classes", () => {
|
||||
const html = render("en");
|
||||
// The chip class composition (`border-dracula-pink/50`,
|
||||
// `bg-dracula-pink/15`, `text-dracula-pink`) is what gives the
|
||||
// The chip class composition (`border-brand-pink/50`,
|
||||
// `bg-brand-pink/15`, `text-brand-pink`) is what gives the
|
||||
// badge its iOS-accent identity. Asserting all three keeps the
|
||||
// colour contract from drifting silently.
|
||||
expect(html).toMatch(/border-dracula-pink\/50/);
|
||||
expect(html).toMatch(/bg-dracula-pink\/15/);
|
||||
expect(html).toMatch(/text-dracula-pink/);
|
||||
expect(html).toMatch(/border-brand-pink\/50/);
|
||||
expect(html).toMatch(/bg-brand-pink\/15/);
|
||||
expect(html).toMatch(/text-brand-pink/);
|
||||
});
|
||||
|
||||
it("renders the localised label in German too", () => {
|
||||
|
||||
@@ -274,7 +274,7 @@ function dayBoundaryIso(
|
||||
*/
|
||||
function sourceBadgeClass(source: string): string {
|
||||
if (source === "APPLE_HEALTH") {
|
||||
return "border-dracula-pink/50 bg-dracula-pink/15 text-dracula-pink";
|
||||
return "border-brand-pink/50 bg-brand-pink/15 text-brand-pink";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -1145,7 +1145,7 @@ export function MeasurementList({
|
||||
<ListRow
|
||||
key={m.id}
|
||||
data-state={isSelected ? "selected" : undefined}
|
||||
className="bg-card border-border data-[state=selected]:border-dracula-purple/60 data-[state=selected]:bg-dracula-purple/5"
|
||||
className="bg-card border-border data-[state=selected]:border-primary/60 data-[state=selected]:bg-primary/5"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
|
||||
@@ -87,7 +87,7 @@ function ComplianceMetaRow({
|
||||
* The streak flame uses the semantic `text-warning` token (an alias over
|
||||
* Dracula orange in dark mode, AA-safe on the light card). The generic card
|
||||
* historically drifted onto Tailwind stock `text-orange-400`, and the flame
|
||||
* later carried the raw `text-dracula-orange` palette token; v1.12.2 routes
|
||||
* later carried the raw `text-warning` palette token; v1.12.2 routes
|
||||
* it through the semantic vocabulary the rest of the status surface uses.
|
||||
*/
|
||||
export function MedicationComplianceBars({
|
||||
|
||||
@@ -125,7 +125,7 @@ export function ResearchModeAcknowledgmentDialog({
|
||||
onOpenChange={onOpenChange}
|
||||
title={
|
||||
<span className="flex items-center gap-2">
|
||||
<BookOpenCheck className="text-dracula-purple h-5 w-5 shrink-0" />
|
||||
<BookOpenCheck className="text-primary h-5 w-5 shrink-0" />
|
||||
{t("medications.researchMode.dialog.title")}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ export function ScheduleHistoryTimeline({
|
||||
<ol className="space-y-0">
|
||||
{/* Live plan — the accent node. */}
|
||||
<TimelineRow
|
||||
dotClassName="bg-dracula-green"
|
||||
dotClassName="bg-success"
|
||||
railVisible={expanded && revisions.length > 0}
|
||||
dataSlot="zeitplan-history-current"
|
||||
>
|
||||
@@ -208,7 +208,7 @@ export function ScheduleHistoryTimeline({
|
||||
: t("medications.detail.zeitplan.history.noTimes")}
|
||||
{revision.source === "MANUAL" && (
|
||||
<span
|
||||
className="border-dracula-purple/30 bg-dracula-purple/10 text-dracula-purple ml-2 inline-flex rounded-full border px-2 py-px align-middle text-[11px] font-medium"
|
||||
className="border-primary/30 bg-primary/10 text-primary ml-2 inline-flex rounded-full border px-2 py-px align-middle text-[11px] font-medium"
|
||||
data-slot="zeitplan-history-manual-chip"
|
||||
>
|
||||
{t("medications.detail.zeitplan.history.manualChip")}
|
||||
@@ -240,7 +240,7 @@ export function ScheduleHistoryTimeline({
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-dracula-red size-8"
|
||||
className="text-muted-foreground hover:text-destructive size-8"
|
||||
aria-label={t(
|
||||
"medications.detail.zeitplan.history.deleteAria",
|
||||
)}
|
||||
@@ -569,7 +569,7 @@ function EraDialog({
|
||||
</p>
|
||||
{validationKey !== null && (
|
||||
<p
|
||||
className="text-dracula-red text-sm"
|
||||
className="text-destructive text-sm"
|
||||
role="alert"
|
||||
data-slot="zeitplan-history-validation"
|
||||
>
|
||||
|
||||
@@ -688,7 +688,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
key={entry.id}
|
||||
data-testid="mood-row"
|
||||
data-state={isSelected ? "selected" : undefined}
|
||||
className="bg-card border-border data-[state=selected]:border-dracula-purple/60 data-[state=selected]:bg-dracula-purple/5 flex items-center justify-between rounded-lg border p-3"
|
||||
className="bg-card border-border data-[state=selected]:border-primary/60 data-[state=selected]:bg-primary/5 flex items-center justify-between rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{/* v1.15.13 MEDIUM-1 kept the 16px Radix Checkbox
|
||||
@@ -1098,7 +1098,7 @@ function MoodNoteText({
|
||||
aria-expanded={expanded}
|
||||
aria-controls={noteId}
|
||||
data-testid="mood-note-toggle"
|
||||
className="text-dracula-purple cursor-pointer text-[11px] font-medium"
|
||||
className="text-primary cursor-pointer text-[11px] font-medium"
|
||||
>
|
||||
{expanded ? t("mood.noteCollapse") : t("mood.noteExpand")}
|
||||
</button>
|
||||
|
||||
@@ -74,7 +74,7 @@ export function DoneScreen() {
|
||||
<div className="border-border/60 bg-muted/30 mx-auto flex w-full max-w-md flex-col items-center gap-2 rounded-lg border p-4 text-center">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="from-dracula-purple to-dracula-pink flex size-9 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
className="from-primary to-brand-pink flex size-9 items-center justify-center rounded-full bg-gradient-to-br"
|
||||
>
|
||||
<Sparkles className="text-background size-4" />
|
||||
</span>
|
||||
|
||||
@@ -192,7 +192,7 @@ export function AboutMeSection({
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"h-1 w-5 rounded-full transition-colors",
|
||||
i < answered ? "bg-dracula-purple" : "bg-border",
|
||||
i < answered ? "bg-primary" : "bg-border",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
@@ -272,11 +272,11 @@ export function AboutMeSection({
|
||||
{pendingQuestions.length > 0 && (
|
||||
<div
|
||||
data-testid="settings-about-me-questions"
|
||||
className="border-dracula-purple/30 bg-dracula-purple/5 rounded-lg border p-4"
|
||||
className="border-primary/30 bg-primary/5 rounded-lg border p-4"
|
||||
>
|
||||
<p className="flex items-center gap-2 text-sm font-medium">
|
||||
<MessageCircleQuestion
|
||||
className="text-dracula-purple size-4 shrink-0"
|
||||
className="text-primary size-4 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("settings.ai.aboutMe.questionsTitle")}
|
||||
@@ -289,7 +289,7 @@ export function AboutMeSection({
|
||||
<p className="mt-3 text-xs">
|
||||
<Link
|
||||
href="/coach"
|
||||
className="text-dracula-purple underline-offset-4 hover:underline"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("settings.ai.aboutMe.questionsOpenCoach")}
|
||||
</Link>
|
||||
@@ -318,7 +318,7 @@ export function AboutMeSection({
|
||||
{t("settings.ai.aboutMe.recordsLink")}{" "}
|
||||
<Link
|
||||
href="/settings/anamnesis"
|
||||
className="text-dracula-purple underline-offset-4 hover:underline"
|
||||
className="text-primary underline-offset-4 hover:underline"
|
||||
>
|
||||
{t("settings.ai.aboutMe.recordsLinkCta")}
|
||||
</Link>
|
||||
|
||||
@@ -67,7 +67,7 @@ export function CoachRemindersSection({
|
||||
data-status={r.status}
|
||||
className={
|
||||
highlighted
|
||||
? "border-dracula-purple/40 bg-dracula-purple/5 flex flex-col gap-2 rounded-lg border p-3"
|
||||
? "border-primary/40 bg-primary/5 flex flex-col gap-2 rounded-lg border p-3"
|
||||
: "border-border bg-background flex flex-col gap-2 rounded-lg border p-3"
|
||||
}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ function render(props: Parameters<typeof RangeBar>[0]) {
|
||||
}
|
||||
|
||||
describe("<RangeBar>", () => {
|
||||
it("renders the bar slot + green/orange/red zones with Dracula tokens", () => {
|
||||
it("renders the bar slot + green/orange/red zones with semantic tokens", () => {
|
||||
const html = render({
|
||||
value: 72,
|
||||
min: 60,
|
||||
@@ -28,9 +28,9 @@ describe("<RangeBar>", () => {
|
||||
unit: "bpm",
|
||||
});
|
||||
expect(html).toContain('data-slot="target-range-bar"');
|
||||
expect(html).toContain("bg-dracula-green/20");
|
||||
expect(html).toContain("bg-dracula-orange/15");
|
||||
expect(html).toContain("bg-dracula-red/10");
|
||||
expect(html).toContain("bg-success/20");
|
||||
expect(html).toContain("bg-warning/15");
|
||||
expect(html).toContain("bg-destructive/10");
|
||||
// Pin the absence of the legacy raw Tailwind palette so the swap
|
||||
// doesn't silently regress on future merges.
|
||||
expect(html).not.toContain("bg-green-500/20");
|
||||
@@ -38,23 +38,23 @@ describe("<RangeBar>", () => {
|
||||
expect(html).not.toContain("bg-red-500/8");
|
||||
});
|
||||
|
||||
it("paints the marker with the green token when the value is in range", () => {
|
||||
it("paints the marker with the success token when the value is in range", () => {
|
||||
const html = render({
|
||||
value: 80,
|
||||
min: 60,
|
||||
max: 100,
|
||||
unit: "bpm",
|
||||
});
|
||||
expect(html).toContain("var(--dracula-green)");
|
||||
expect(html).toContain("var(--success)");
|
||||
});
|
||||
|
||||
it("paints the marker with the red token when the value is out of band", () => {
|
||||
it("paints the marker with the destructive token when the value is out of band", () => {
|
||||
const html = render({
|
||||
value: 180,
|
||||
min: 60,
|
||||
max: 100,
|
||||
unit: "bpm",
|
||||
});
|
||||
expect(html).toContain("var(--dracula-red)");
|
||||
expect(html).toContain("var(--destructive)");
|
||||
});
|
||||
});
|
||||
@@ -54,9 +54,9 @@ export interface ConsistencyStripProps {
|
||||
// encoded inside the value via color-mix so the opacity modifier is
|
||||
// no longer needed and the bracketed form parses cleanly.
|
||||
const BAND_STYLES: Record<NonNullable<ConsistencyBand>, string> = {
|
||||
in: "bg-(--dracula-green) border-(--dracula-green) shadow-[0_0_0_1px_color-mix(in_oklab,var(--dracula-green)_20%,transparent)]",
|
||||
near: "bg-(--dracula-orange) border-(--dracula-orange)",
|
||||
out: "bg-(--dracula-red) border-(--dracula-red)",
|
||||
in: "bg-success border-success shadow-[0_0_0_1px_color-mix(in_oklab,var(--success)_20%,transparent)]",
|
||||
near: "bg-warning border-warning",
|
||||
out: "bg-destructive border-destructive",
|
||||
};
|
||||
|
||||
const BAND_ARIA: Record<NonNullable<ConsistencyBand>, string> = {
|
||||
|
||||
@@ -76,10 +76,10 @@ export function RangeBar({
|
||||
!inGreen && value >= effectiveOrangeMin && value <= effectiveOrangeMax;
|
||||
|
||||
const markerColor = inGreen
|
||||
? "var(--dracula-green)"
|
||||
? "var(--success)"
|
||||
: inYellow
|
||||
? "var(--dracula-orange)"
|
||||
: "var(--dracula-red)";
|
||||
? "var(--warning)"
|
||||
: "var(--destructive)";
|
||||
const minLabelPosition = Math.max(5, Math.min(95, greenStart));
|
||||
const maxLabelPosition = Math.max(5, Math.min(95, greenEnd));
|
||||
|
||||
@@ -99,18 +99,18 @@ export function RangeBar({
|
||||
chart palette stays aligned with PR-badge, alerts, and the
|
||||
marker dot itself. Raw Tailwind palettes drift from the
|
||||
theme over time and never get dark-mode tuned. */}
|
||||
<div className="bg-dracula-red/10 absolute inset-0 rounded-full" />
|
||||
<div className="bg-destructive/10 absolute inset-0 rounded-full" />
|
||||
{/* Orange (caution) side zones — `--dracula-orange` matches the
|
||||
marker's out-of-band tint. */}
|
||||
<div
|
||||
className="bg-dracula-orange/15 absolute top-0 h-full"
|
||||
className="bg-warning/15 absolute top-0 h-full"
|
||||
style={{
|
||||
left: `${yellowLeftStart}%`,
|
||||
width: `${greenStart - yellowLeftStart}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="bg-dracula-orange/15 absolute top-0 h-full"
|
||||
className="bg-warning/15 absolute top-0 h-full"
|
||||
style={{
|
||||
left: `${greenEnd}%`,
|
||||
width: `${yellowRightEnd - greenEnd}%`,
|
||||
@@ -118,7 +118,7 @@ export function RangeBar({
|
||||
/>
|
||||
{/* In-band green zone — `--dracula-green`. */}
|
||||
<div
|
||||
className="bg-dracula-green/20 absolute top-0 h-full"
|
||||
className="bg-success/20 absolute top-0 h-full"
|
||||
style={{
|
||||
left: `${greenStart}%`,
|
||||
width: `${greenEnd - greenStart}%`,
|
||||
|
||||
@@ -26,7 +26,7 @@ function Checkbox({
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-border/70 size-4 shrink-0 rounded border",
|
||||
"data-[state=checked]:bg-dracula-purple data-[state=checked]:border-dracula-purple data-[state=checked]:text-background",
|
||||
"data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-background",
|
||||
"focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"transition-colors",
|
||||
|
||||
Reference in new issue
Block a user