fix(ui): drop themed text colours from the last three admin surfaces

SystemHealthPage, CrmOverviewSection and HoursSection used text-theme
(color: var(--color-text)) explicitly, so on a dark-toned branding theme they
render near-invisible on the light admin background -- and because the class
is explicit it beats the AdminLayout default that protects everything else.

Converted to the neutral scale using the convention from da9ceb14. Swept the
whole of each file rather than the cited lines: 3 in SystemHealthPage, 5 in
CrmOverviewSection (two of them h3 elements with no colour class at all), 17
in HoursSection. All three are admin-only -- call sites verified as
AdminDashboard, CustomerDetailPage, HoursLoggingPage and /admin/system-health
-- so no customer-portal or public-token surface is affected, where these
utilities are correct by design. Zero themed utilities remain in the three
files.

Refs testplan REPORT.md B14.
This commit is contained in:
Paul Nothaft
2026-09-02 09:43:10 +02:00
parent bd44708a03
commit 22cada9082
4 changed files with 48 additions and 25 deletions
@@ -85,7 +85,7 @@ export const CrmOverviewSection: React.FC = () => {
// backend). Keep it small so it doesn't disrupt the page.
return (
<section className="mt-8">
<h2 className="text-xl font-bold text-theme mb-2">
<h2 className="text-xl font-bold text-neutral-900 dark:text-neutral-100 mb-2">
{t('crmOverview.title', 'CRM overview')}
</h2>
<p className="text-sm text-red-600">
@@ -101,7 +101,7 @@ export const CrmOverviewSection: React.FC = () => {
return (
<section className="mt-8 space-y-5">
<h2 className="text-xl font-bold text-theme">
<h2 className="text-xl font-bold text-neutral-900 dark:text-neutral-100">
{t('crmOverview.title', 'CRM overview')}
</h2>
@@ -155,7 +155,7 @@ export const CrmOverviewSection: React.FC = () => {
{quotesBlock && (
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold flex items-center gap-2">
<h3 className="text-base font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<FileText className="w-5 h-5" />
{t('crmOverview.quotes.title', 'Quotes')}
</h3>
@@ -208,7 +208,7 @@ export const CrmOverviewSection: React.FC = () => {
{invoicesBlock && (
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold flex items-center gap-2">
<h3 className="text-base font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Receipt className="w-5 h-5" />
{t('crmOverview.invoices.title', 'Invoices')}
</h3>
@@ -270,9 +270,9 @@ const StatCard: React.FC<StatCardProps> = ({ icon, label, value, sub, to, onClic
<div className="flex items-start gap-3">
<div className="shrink-0 mt-0.5">{icon}</div>
<div className="min-w-0">
<div className="text-xs uppercase tracking-wider text-muted-theme">{label}</div>
<div className="text-xs uppercase tracking-wider text-neutral-500 dark:text-neutral-400">{label}</div>
<div className="text-2xl font-bold tabular-nums mt-1">{value}</div>
{sub && <div className="text-xs text-muted-theme mt-1">{sub}</div>}
{sub && <div className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{sub}</div>}
</div>
</div>
</Card>
+20 -16
View File
@@ -248,11 +248,15 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
return (
<Card padding="lg">
<h2 className="text-lg font-semibold text-theme mb-1 flex items-center gap-2">
{/* Explicit neutral colours (not `text-theme` / `text-muted-theme`):
those resolve to the gallery branding theme's --color-text, which
is applied globally on <html> and renders near-white inside the
light admin chrome (QA S13). */}
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-1 flex items-center gap-2">
<Clock className="w-5 h-5" />
{t('customers.hours.section', 'Hours')}
</h2>
<p className="text-xs text-muted-theme mb-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-4">
{isMonthly
? t('customers.hours.monthlyHint',
'Entries auto-append to the current monthly draft. Edit / delete remains possible until the scheduler arms the draft for send.')
@@ -268,7 +272,7 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
is configured anywhere along the chain. */}
{!compact && (
<div className="mb-4">
<label className="block text-sm font-medium text-theme mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('customers.field.hourlyRate', 'Default hourly rate')}
</label>
{onHourlyRateChange ? (
@@ -283,7 +287,7 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
className="w-40 input"
placeholder="150.00"
/>
<p className="text-xs text-muted-theme mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('customers.field.hourlyRateHint',
'Major units (e.g. 150.00 for {{currency}} 150). Leave blank to require a per-entry override on every block.',
{ currency: profileDefaultCurrency })}
@@ -315,11 +319,11 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
</div>
</div>
) : (
<p className="text-sm text-theme">
<p className="text-sm text-neutral-900 dark:text-neutral-100">
<span className="tabular-nums font-medium">
{profileDefaultCurrency} {((effectiveDefaultRateMinor as number) / 100).toFixed(2)}
</span>
<span className="text-xs text-muted-theme ml-2">
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2">
{customerHourlyRateMinor != null
? t('customers.hours.rateSource.customer', 'from this customer')
: t('customers.hours.rateSource.installDefault', 'install-wide default')}
@@ -333,28 +337,28 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
lives on the standalone /admin/clients/hours surface. */}
{!compact && (
<div className="border-t border-neutral-200 dark:border-neutral-700 pt-4 mb-4">
<h3 className="text-sm font-semibold mb-3">{t('customers.hours.form.title', 'Log new entry')}</h3>
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-3">{t('customers.hours.form.title', 'Log new entry')}</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-3">
<div>
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('customers.hours.form.date', 'Date')}
</label>
<LocalizedDateInput value={entryDate} onChange={setEntryDate} />
</div>
<div>
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('customers.hours.form.start', 'Start')}
</label>
<TimeField value={startTime} onChange={setStartTime} />
</div>
<div>
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('customers.hours.form.end', 'End')}
</label>
<TimeField value={endTime} onChange={setEndTime} />
</div>
<div>
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('customers.hours.form.duration', 'Duration')}
</label>
<input
@@ -375,7 +379,7 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
className="input w-full" />
</div>
<div>
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('customers.hours.form.rateOverride', 'Rate override')}
</label>
<input
@@ -390,7 +394,7 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
</div>
</div>
<div className="mt-3">
<label className="block text-xs text-muted-theme mb-1">
<label className="block text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{t('customers.hours.form.note', 'Note / description')}
</label>
<textarea rows={2} value={description}
@@ -460,16 +464,16 @@ export const HoursSection: React.FC<HoursSectionProps> = ({
{/* Entry list table. */}
{isLoading ? (
<p className="text-sm text-muted-theme">{t('common.loading', 'Loading…')}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">{t('common.loading', 'Loading…')}</p>
) : entries.length === 0 ? (
<p className="text-sm text-muted-theme">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('customers.hours.empty', 'No entries logged yet.')}
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs uppercase text-muted-theme">
<tr className="text-left text-xs uppercase text-neutral-500 dark:text-neutral-400">
<th className="py-2 pr-3">{t('customers.hours.col.date', 'Date')}</th>
<th className="py-2 pr-3">{t('customers.hours.col.range', 'Time')}</th>
<th className="py-2 pr-3 text-right">{t('customers.hours.col.hours', 'Hours')}</th>