fix(crm): admin surfaces follow the admin light/dark toggle, not the gallery theme (#620)
CRM + accounting admin surfaces read gallery-theme tokens — the `text-theme`/`text-muted-theme` utility classes and raw `var(--color-surface| text|surface-border|elevated)` inline styles — which `ThemeContext` writes as inline `--color-*` on <html> for every route. Those inline vars beat the admin `.dark` toggle, so e.g. the "Create passive customer" modal (#620) renders dark while the admin is in light mode (and the reverse). Repoint every CRM/accounting admin surface to Tailwind `dark:` classes so it tracks the admin toggle deterministically: - text-theme → text-neutral-900 dark:text-neutral-100; text-muted-theme → text-neutral-500 dark:text-neutral-400 (across the CRM list/detail/editor pages, hours, calendar, lineage, installments, CRM settings). - modal/dropdown/chip/divider inline `var(--color-surface*)` → bg-white dark:bg-neutral-900 / border-neutral-200 dark:border-neutral-700 etc. (CustomerManagement + CustomerDetail modals = the #620 fix). - toggle off-track surface-border → bg-neutral-300 dark:bg-neutral-600; brand accent ON-state kept (var(--color-accent)). - CalendarPage FullCalendar chrome: scope local --cal-* vars under .fc / .dark .fc so the calendar follows the admin toggle (was reading gallery vars). - PasswordResetModal bare neutrals + TaxReport Storno/Reissue badges gain dark pairings. Brand accent/primary tokens and the branded admin login are intentionally left on the gallery theme. The same leak exists in non-CRM admin areas (dashboard, events) — out of scope here.
This commit is contained in:
@@ -120,10 +120,10 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<label className="block text-sm font-medium text-theme mb-1">
|
||||
<label className="block text-sm font-medium text-neutral-900 dark:text-neutral-100 mb-1">
|
||||
{t('events.customerPicker.label', 'Customer accounts')}
|
||||
</label>
|
||||
<p className="text-xs text-muted-theme mb-2">{helpText}</p>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">{helpText}</p>
|
||||
|
||||
{/* Selected chips */}
|
||||
{value.length > 0 && (
|
||||
@@ -131,16 +131,11 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
|
||||
{value.map((c) => (
|
||||
<span
|
||||
key={c.id}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-elevated, #f5f5f5)',
|
||||
color: 'var(--color-text)',
|
||||
border: '1px solid var(--color-surface-border, #e5e5e5)',
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 border border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
<span className="font-medium">{c.displayName?.trim() || c.email}</span>
|
||||
{c.displayName?.trim() && c.email !== c.displayName && (
|
||||
<span className="text-muted-theme">· {c.email}</span>
|
||||
<span className="text-neutral-500 dark:text-neutral-400">· {c.email}</span>
|
||||
)}
|
||||
{!disabled && (
|
||||
<button
|
||||
@@ -174,18 +169,14 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
|
||||
{/* Dropdown */}
|
||||
{isOpen && query.trim() !== '' && (
|
||||
<div
|
||||
className="absolute left-0 right-0 mt-1 z-20 rounded-lg shadow-lg border max-h-72 overflow-y-auto"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-surface, #ffffff)',
|
||||
borderColor: 'var(--color-surface-border, #e5e5e5)',
|
||||
}}
|
||||
className="absolute left-0 right-0 mt-1 z-20 rounded-lg shadow-lg border max-h-72 overflow-y-auto bg-white dark:bg-neutral-900 border-neutral-200 dark:border-neutral-700"
|
||||
>
|
||||
{isSearching ? (
|
||||
<div className="px-3 py-3 text-sm text-muted-theme">
|
||||
<div className="px-3 py-3 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{t('events.customerPicker.searching', 'Searching…')}
|
||||
</div>
|
||||
) : results.length === 0 ? (
|
||||
<div className="px-3 py-3 text-sm text-muted-theme">
|
||||
<div className="px-3 py-3 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{t('events.customerPicker.noResults', 'No matches. Invite this customer from Clients → Accounts first.')}
|
||||
</div>
|
||||
) : (
|
||||
@@ -197,7 +188,7 @@ export const CustomerAccountPicker: React.FC<Props> = ({ value, onChange, disabl
|
||||
onClick={() => select(r)}
|
||||
className="w-full text-left px-3 py-2 text-sm hover:bg-neutral-100 dark:hover:bg-neutral-700 flex items-center gap-2"
|
||||
>
|
||||
<UserPlus className="w-4 h-4 text-muted-theme flex-shrink-0" />
|
||||
<UserPlus className="w-4 h-4 text-neutral-500 dark:text-neutral-400 flex-shrink-0" />
|
||||
<span className="flex-1 truncate">{labelFor(r)}</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -57,7 +57,7 @@ const QuotesPanel: React.FC<Props> = ({ customerAccountId }) => {
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" /> {t('customers.detail.quotesSection', 'Quotes')}
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
@@ -78,18 +78,18 @@ const QuotesPanel: React.FC<Props> = ({ customerAccountId }) => {
|
||||
</div>
|
||||
|
||||
{isLoading ? <Loading /> : !data || data.quotes.length === 0 ? (
|
||||
<p className="text-sm text-muted-theme">
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{t('customers.detail.noQuotes', 'No quotes for this customer yet.')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
|
||||
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
{data.quotes.map((q) => (
|
||||
<li key={q.id} className="py-2 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link to={`/admin/clients/quotes/${q.id}`} className="text-theme hover:underline font-mono text-sm">
|
||||
<Link to={`/admin/clients/quotes/${q.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline font-mono text-sm">
|
||||
{q.quoteNumber}
|
||||
</Link>
|
||||
<span className="text-xs text-muted-theme ml-2">{q.eventName || fmtDate(q.issueDate)}</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2">{q.eventName || fmtDate(q.issueDate)}</span>
|
||||
</div>
|
||||
<span className="text-sm tabular-nums">{formatMoney(Number(q.totalAmountMinor) / 100, q.currency)}</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
@@ -118,7 +118,7 @@ const ContractsPanel: React.FC<Props> = ({ customerAccountId }) => {
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<ScrollText className="w-5 h-5" /> {t('customers.detail.contractsSection', 'Contracts')}
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
@@ -132,18 +132,18 @@ const ContractsPanel: React.FC<Props> = ({ customerAccountId }) => {
|
||||
</div>
|
||||
|
||||
{isLoading ? <Loading /> : !data || data.contracts.length === 0 ? (
|
||||
<p className="text-sm text-muted-theme">
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{t('customers.detail.noContracts', 'No contracts for this customer yet.')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
|
||||
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
{data.contracts.map((c) => (
|
||||
<li key={c.id} className="py-2 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link to={`/admin/clients/contracts/${c.id}`} className="text-theme hover:underline font-mono text-sm">
|
||||
<Link to={`/admin/clients/contracts/${c.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline font-mono text-sm">
|
||||
{c.contractNumber}
|
||||
</Link>
|
||||
<span className="text-xs text-muted-theme ml-2 truncate">{c.title || fmtDate(c.issueDate)}</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2 truncate">{c.title || fmtDate(c.issueDate)}</span>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
c.status === 'fully_signed' ? 'bg-green-100 text-green-800'
|
||||
@@ -172,7 +172,7 @@ const InvoicesPanel: React.FC<Props> = ({ customerAccountId }) => {
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold text-theme flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
|
||||
<Receipt className="w-5 h-5" /> {t('customers.detail.billsSection', 'Invoices')}
|
||||
</h2>
|
||||
<div className="flex gap-2">
|
||||
@@ -187,18 +187,18 @@ const InvoicesPanel: React.FC<Props> = ({ customerAccountId }) => {
|
||||
</div>
|
||||
|
||||
{isLoading ? <Loading /> : !data || data.invoices.length === 0 ? (
|
||||
<p className="text-sm text-muted-theme">
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{t('customers.detail.noBills', 'No invoices for this customer yet.')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="divide-y" style={{ borderColor: 'var(--color-surface-border)' }}>
|
||||
<ul className="divide-y divide-neutral-200 dark:divide-neutral-700">
|
||||
{data.invoices.map((inv) => (
|
||||
<li key={inv.id} className="py-2 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link to={`/admin/clients/bills/${inv.id}`} className="text-theme hover:underline font-mono text-sm">
|
||||
<Link to={`/admin/clients/bills/${inv.id}`} className="text-neutral-900 dark:text-neutral-100 hover:underline font-mono text-sm">
|
||||
{inv.invoiceNumber}
|
||||
</Link>
|
||||
<span className="text-xs text-muted-theme ml-2">
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400 ml-2">
|
||||
{fmtDate(inv.dueDate)}
|
||||
{inv.installmentTotal > 1 ? ` · ${inv.installmentIndex + 1}/${inv.installmentTotal}` : ''}
|
||||
</span>
|
||||
|
||||
@@ -62,8 +62,8 @@ const Toggle: React.FC<ToggleProps> = ({ enabled, onChange, label, hint, icon: I
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={onChange}
|
||||
className="relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
style={{ backgroundColor: enabled ? 'var(--color-accent, #5C8762)' : '#cbd5e1' }}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${enabled ? '' : 'bg-neutral-300 dark:bg-neutral-600'}`}
|
||||
style={enabled ? { backgroundColor: 'var(--color-accent, #5C8762)' } : undefined}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${enabled ? 'translate-x-6' : 'translate-x-1'}`}
|
||||
|
||||
@@ -91,7 +91,7 @@ export const DocumentLineageCard: React.FC<DocumentLineageCardProps> = ({
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card padding="md" className={className}>
|
||||
<p className="text-sm text-muted-theme">
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
{t('dealLineage.loading', 'Loading related documents…')}
|
||||
</p>
|
||||
</Card>
|
||||
@@ -128,7 +128,7 @@ export const DocumentLineageCard: React.FC<DocumentLineageCardProps> = ({
|
||||
<h2 className="font-semibold mb-1 flex items-center gap-2">
|
||||
{t('dealLineage.title', 'Related documents')}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-theme">
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{t('dealLineage.empty', 'No other documents share this deal yet. New invoices, contracts, or installments will show up here once created.')}
|
||||
</p>
|
||||
</Card>
|
||||
@@ -256,7 +256,7 @@ const Group: React.FC<{
|
||||
action?: React.ReactNode;
|
||||
}> = ({ icon, label, count, children, action }) => (
|
||||
<div className="mb-3 last:mb-0">
|
||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wider text-muted-theme mb-1">
|
||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wider text-neutral-500 dark:text-neutral-400 mb-1">
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
<span>({count})</span>
|
||||
@@ -282,19 +282,19 @@ const Row: React.FC<{
|
||||
const inner = (
|
||||
<div className="flex items-center justify-between gap-3 py-1.5">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className={`font-mono text-sm ${isCurrent ? 'text-muted-theme' : ''}`}>{number}</span>
|
||||
<span className={`font-mono text-sm ${isCurrent ? 'text-neutral-500 dark:text-neutral-400' : ''}`}>{number}</span>
|
||||
{badge && (
|
||||
<span className="text-[10px] uppercase tracking-wider px-1.5 py-0.5 rounded font-semibold bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
{meta && (
|
||||
<span className="text-xs text-muted-theme truncate">{meta}</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400 truncate">{meta}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0 text-xs">
|
||||
{right && <span className="tabular-nums">{right}</span>}
|
||||
<span className="text-muted-theme">{t(statusKey, statusFallback)}</span>
|
||||
<span className="text-neutral-500 dark:text-neutral-400">{t(statusKey, statusFallback)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -153,7 +153,7 @@ export const EditInstallmentPlanModal: React.FC<EditInstallmentPlanModalProps> =
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('dealLineage.editPlanModalTitle', 'Edit installment plan')}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-theme mt-1">
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
|
||||
{t('dealLineage.editPlanHelp',
|
||||
'Atomically reshape this plan: change percents, labels, triggers, add or remove rows. The plan total stays fixed; existing invoice numbers are kept where possible. Refused once any invoice has shipped.')}
|
||||
</p>
|
||||
|
||||
@@ -177,7 +177,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
|
||||
|
||||
{enabled && (
|
||||
<>
|
||||
<p className="text-xs text-muted-theme mb-3">
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-3">
|
||||
{advanced
|
||||
? t('installments.advancedHint',
|
||||
'Pick a trigger (quote accepted, before/after event, on delivery, fixed date) plus offset in days. Triggers re-resolve if the event date later shifts.')
|
||||
@@ -192,7 +192,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
|
||||
className="grid grid-cols-12 gap-2 items-end p-2 rounded-md bg-neutral-50 dark:bg-neutral-800/40"
|
||||
>
|
||||
<div className="col-span-2">
|
||||
<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('installments.percent', '%')}
|
||||
</label>
|
||||
<Input
|
||||
@@ -206,7 +206,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<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('installments.label', 'Label')}
|
||||
</label>
|
||||
<Input
|
||||
@@ -219,11 +219,11 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
|
||||
|
||||
{!advanced ? (
|
||||
<div className="col-span-5">
|
||||
<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('installments.sendOn', 'Send on')}
|
||||
</label>
|
||||
{row.trigger === 'after_delivery' ? (
|
||||
<div className="text-xs text-muted-theme py-2">
|
||||
<div className="text-xs text-neutral-500 dark:text-neutral-400 py-2">
|
||||
{t('installments.onDeliveryHint',
|
||||
'On delivery — admin releases manually. Switch to advanced to change.')}
|
||||
</div>
|
||||
@@ -242,7 +242,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
|
||||
) : (
|
||||
<>
|
||||
<div className="col-span-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('installments.trigger', 'Trigger')}
|
||||
</label>
|
||||
<select
|
||||
@@ -262,7 +262,7 @@ export const InstallmentsPanel: React.FC<InstallmentsPanelProps> = ({
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<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('installments.offsetDays', 'Offset (days)')}
|
||||
</label>
|
||||
<Input
|
||||
|
||||
@@ -87,12 +87,12 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<Card className="max-w-md w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-neutral-900">
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
{resultPassword ? t('events.passwordReset.newTitle') : t('events.passwordReset.title')}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-neutral-400 hover:text-neutral-600"
|
||||
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -163,12 +163,12 @@ export const PasswordResetModal: React.FC<PasswordResetModalProps> = ({
|
||||
type="checkbox"
|
||||
checked={sendEmail}
|
||||
onChange={(e) => setSendEmail(e.target.checked)}
|
||||
className="w-4 h-4 text-accent bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500 focus:ring-2"
|
||||
className="w-4 h-4 text-accent bg-neutral-100 dark:bg-neutral-700 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500 focus:ring-2"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-neutral-500" />
|
||||
<span className="text-sm font-medium text-neutral-700">
|
||||
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||
{t('events.passwordReset.sendEmail')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user