fix: enforce semantic contrast across admin
Local Build and Deploy / deploy (push) Successful in 44s

This commit is contained in:
Simo committed 2026-07-12 15:14:15 +02:00
1 parent 45c8178cfa
commit f0b4bc1630
86 files changed
+438 -338

No files matched your search

+3 -3
View File
@@ -27,12 +27,12 @@ export function AdminNavLink({
href={href}
className={`flex items-center gap-2.5 px-2.5 py-2 rounded-xl text-sm font-medium no-underline transition-all duration-150 ${
active
? "bg-gradient-to-r from-[var(--color-primary)]/20 to-transparent text-white shadow-sm border-l-2 border-[var(--color-primary)]"
: "text-[#8b8fa8] hover:text-white hover:bg-white/[0.04] hover:no-underline"
? "bg-[var(--color-primary)]/15 text-[var(--admin-sidebar-text-readable)] shadow-sm border-l-2 border-[var(--color-primary)]"
: "text-[var(--admin-sidebar-muted-readable)] hover:text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--color-primary)]/10 hover:no-underline"
}`}
>
{icon ? (
<span className={`flex-none ${active ? "text-[var(--color-primary)]" : "text-[#5c5f7a]"}`}>
<span className={`flex-none ${active ? "text-[var(--color-primary-readable)]" : "text-[var(--admin-sidebar-muted-readable)]"}`}>
{icon}
</span>
) : null}
@@ -287,7 +287,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
{isDirty && (
<Badge
variant="outline"
className="h-4 px-1.5 text-[9px] text-orange-600 border-orange-300 bg-orange-50 dark:bg-orange-950/30 dark:border-orange-700"
className="h-4 px-1.5 text-[9px] text-[var(--color-warning-text-readable)] border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)]"
>
<AlertCircle className="h-2.5 w-2.5 mr-0.5" />
unsaved
@@ -310,7 +310,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
{/* Dirty banner */}
{isDirty && (
<div className="flex items-center gap-2 px-4 py-1.5 bg-yellow-50 dark:bg-yellow-950/30 border-b border-yellow-200 dark:border-yellow-800 text-xs text-yellow-800 dark:text-yellow-200 shrink-0">
<div className="flex items-center gap-2 px-4 py-1.5 bg-[var(--color-warning-subtle)] border-b border-[var(--color-warning-border)] text-xs text-[var(--color-warning-text-readable)] shrink-0">
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
<span className="flex-1">
Unsaved changes — press{' '}
@@ -340,7 +340,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
<TabsTrigger value="settings" className="gap-1.5 text-xs">
<Settings className="h-3.5 w-3.5" />
Settings
{isDirty && <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-orange-500" />}
{isDirty && <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-[var(--color-warning)]" />}
</TabsTrigger>
<TabsTrigger value="items" className="gap-1.5 text-xs">
<Package className="h-3.5 w-3.5" />
@@ -359,7 +359,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
const furniSet = new Set(itemsData.furniDataIdList)
const missing = itemsData.baseItems.filter((b) => !furniSet.has(b.id)).length
return missing > 0 ? (
<span className="ml-0.5 rounded-full bg-orange-500 text-white text-[9px] px-1 py-0.5 leading-none font-medium">
<span className="ml-0.5 rounded-full bg-[var(--color-warning)] text-[var(--color-warning-foreground-readable)] text-[9px] px-1 py-0.5 leading-none font-medium">
{missing}
</span>
) : null
+12 -12
View File
@@ -109,17 +109,17 @@ export function CatalogIcon({ iconImage, size = 20 }: { iconImage: number; size?
/* ─── Layout label helper ────────────────────────────────── */
export const LAYOUT_COLORS: Record<string, string> = {
default_3x3: 'bg-blue-500/15 text-blue-600 dark:text-blue-400',
frontpage: 'bg-amber-500/15 text-amber-600 dark:text-amber-400',
spaces_new: 'bg-green-500/15 text-green-600 dark:text-green-400',
default_3x3: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)]',
frontpage: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]',
spaces_new: 'bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]',
pets: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
pets2: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
pets3: 'bg-pink-500/15 text-pink-600 dark:text-pink-400',
marketplace: 'bg-purple-500/15 text-purple-600 dark:text-purple-400',
recycler: 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400',
club_buy: 'bg-yellow-500/15 text-yellow-600 dark:text-yellow-400',
single_bundle: 'bg-indigo-500/15 text-indigo-600 dark:text-indigo-400',
room_bundle: 'bg-indigo-500/15 text-indigo-600 dark:text-indigo-400',
recycler: 'bg-[var(--color-success-subtle)] text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]',
club_buy: 'bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]',
single_bundle: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)]',
room_bundle: 'bg-[var(--color-info-subtle)] text-[var(--color-info-text-readable)] dark:text-[var(--color-info-text-readable)]',
}
/* ─── Highlight matched text ─────────────────────────────── */
@@ -131,7 +131,7 @@ function HighlightText({ text, query }: { text: string; query: string }) {
return (
<>
{text.slice(0, idx)}
<mark className="bg-yellow-200 dark:bg-yellow-800/60 text-inherit rounded-sm px-0.5">
<mark className="bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] rounded-sm px-0.5">
{text.slice(idx, idx + query.length)}
</mark>
{text.slice(idx + query.length)}
@@ -325,7 +325,7 @@ export function CatalogTree({ rootPages, catalogType = 'normal' }: CatalogTreePr
className={cn(
'px-2 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1',
filterMode === 'enabled'
? 'bg-background shadow-sm text-green-600 dark:text-green-400'
? 'bg-background shadow-sm text-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]'
: 'text-muted-foreground hover:text-foreground',
)}
>
@@ -345,7 +345,7 @@ export function CatalogTree({ rootPages, catalogType = 'normal' }: CatalogTreePr
className={cn(
'px-2 py-1 rounded text-xs font-medium transition-colors flex items-center gap-1',
filterMode === 'disabled'
? 'bg-background shadow-sm text-red-600 dark:text-red-400'
? 'bg-background shadow-sm text-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)]'
: 'text-muted-foreground hover:text-foreground',
)}
>
@@ -643,8 +643,8 @@ function TreeItem({
className={cn(
'flex items-center justify-center h-5 w-5 shrink-0 rounded opacity-0 group-hover:opacity-100 transition-opacity',
isDisabled
? 'hover:bg-green-500/20 text-muted-foreground hover:text-green-600'
: 'hover:bg-red-500/20 text-muted-foreground hover:text-red-600',
? 'hover:bg-[var(--color-success-subtle)] text-muted-foreground hover:text-[var(--color-success-text-readable)]'
: 'hover:bg-[var(--color-error-subtle)] text-muted-foreground hover:text-[var(--color-error-text-readable)]',
)}
>
{isDisabled ? (
@@ -189,7 +189,7 @@ export function BulkImportItems({ pageId, onImported }: BulkImportItemsProps) {
<td className="px-2 py-0.5 font-mono">{row.pointsType ?? '—'}</td>
<td className="px-2 py-0.5">
{row.ok ? (
<span className="text-green-600">OK</span>
<span className="text-[var(--color-success-text-readable)]">OK</span>
) : (
<span className="text-destructive">{row.error}</span>
)}
@@ -75,7 +75,7 @@ export function ItemsShopPreview({
{/* Badges overlay */}
<div className="absolute right-1 top-1 flex flex-col gap-0.5">
{isClub && (
<Badge className="h-4 gap-0.5 border-0 bg-yellow-500/90 px-1 text-[8px] text-white">
<Badge className="h-4 gap-0.5 border-0 bg-[var(--color-warning-subtle)] px-1 text-[8px] text-white">
<Crown className="h-2.5 w-2.5" />
HC
</Badge>
@@ -110,7 +110,7 @@ export function ItemsShopPreview({
</p>
<div className="mt-0.5 flex items-center gap-1.5 text-[10px] font-mono">
{item.costCredits > 0 && (
<span className="text-yellow-600 dark:text-yellow-500">
<span className="text-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]">
{item.costCredits}c
</span>
)}
+7 -7
View File
@@ -61,15 +61,15 @@ export function DiagnosticRow({
const text = state === "ok" ? "OK" : state === "warn" ? "WARN" : "ERROR";
const dotColor =
state === "ok"
? "bg-green-600 shadow-[0_0_0_3px_rgba(22,163,74,0.22)]"
? "bg-[var(--color-success-subtle)] shadow-[0_0_0_3px_rgba(22,163,74,0.22)]"
: state === "warn"
? "bg-[var(--color-primary)] shadow-[0_0_0_3px_rgba(238,180,37,0.22)]"
: "bg-red-500 shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
: "bg-[var(--color-error-subtle)] shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
const badgeColor =
state === "ok"
? "bg-green-600/20"
? "bg-[var(--color-success-subtle)]"
: state === "danger"
? "bg-red-500/22"
? "bg-[var(--color-error-subtle)]"
: "bg-[var(--color-primary)]/18";
return (
@@ -144,7 +144,7 @@ export function OnlineUsersWidget({
/>
{user.roomId && user.roomName && (
<div className="absolute -bottom-0.5 -right-0.5">
<span className="block w-[10px] h-[10px] bg-green-500 rounded-full border-2 border-[var(--color-surface)]" />
<span className="block w-[10px] h-[10px] bg-[var(--color-success-subtle)] rounded-full border-2 border-[var(--color-surface)]" />
</div>
)}
</div>
@@ -160,9 +160,9 @@ export function OnlineUsersWidget({
</div>
<div className="text-[0.75rem] text-[var(--color-text-muted)]">
{user.roomId && user.roomName ? (
<span className="text-green-500 font-medium">🏠 {user.roomName}</span>
<span className="text-[var(--color-success-text-readable)] font-medium">🏠 {user.roomName}</span>
) : (
<span className="text-red-500">⏱️ {formatLastSeen(user.lastActivity)}</span>
<span className="text-[var(--color-error-text-readable)]">⏱️ {formatLastSeen(user.lastActivity)}</span>
)}
</div>
</div>
@@ -292,7 +292,7 @@ export function EmulatorSettingsForm({
<div
key={setting.key}
className={`flex items-center gap-3 px-3 py-1.5 border-l-4 ${borderColor} ${
isChanged ? 'bg-yellow-50/60 dark:bg-yellow-950/20' : ''
isChanged ? 'bg-[var(--color-warning-subtle)] dark:bg-[var(--color-warning-subtle)]' : ''
}`}
>
<code
@@ -305,7 +305,7 @@ export function EmulatorSettingsForm({
value={display}
onChange={(e) => updateValue(setting.key, e.target.value)}
className={`flex-1 h-8 text-xs font-mono ${
isChanged ? 'border-yellow-500/60' : ''
isChanged ? 'border-[var(--color-warning-border)]' : ''
}`}
/>
{isChanged && (
+2 -2
View File
@@ -103,9 +103,9 @@ export function AdminMediaGrid() {
)}
{loading ? (
<p className="text-xs text-gray-500 dark:text-gray-400">Loading…</p>
<p className="text-xs theme-text-muted dark:theme-text-muted">Loading…</p>
) : files.length === 0 ? (
<p className="text-xs text-gray-500 dark:text-gray-400">No images yet. Upload one above.</p>
<p className="text-xs theme-text-muted dark:theme-text-muted">No images yet. Upload one above.</p>
) : (
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3">
{files.map((f) => (
+6
View File
@@ -13,5 +13,11 @@ describe("themePaletteCss", () => {
expect(css).toContain(`--color-background:${ocean.light.color_background}`);
expect(css).toContain(`--color-background:${ocean.dark.color_background}`);
expect(ocean.dark.color_primary).toBe(ocean.light.color_primary);
for (const variable of [
"--color-text-subtle-readable", "--color-text-disabled-readable",
"--admin-sidebar-text-readable", "--color-success-text-readable",
"--color-success-foreground-readable", "--color-warning-text-readable",
"--color-error-text-readable", "--color-info-text-readable",
]) expect(css).toContain(variable);
});
});