feat: enforce no hardcoded colors across entire CMS
Added scripts/check-admin-colors.mjs — scans all src/ files for: - text-white, text-black (use theme text vars) - bg-white, bg-black (use theme background/overlay vars) - bg/text/border/ring with gray/slate/zinc/stone palette - bg/text/border/ring with red/green/blue/etc palette Fixed 21 violations across 11 files: - Overlays: bg-black/* → bg-foreground/* - Text: text-white → text-primary-foreground - Backgrounds: bg-white/10 → bg-background/10 - Green accents: bg-green-* → bg-primary - Red accents: bg-red-* → bg-destructive Integrated into: - lint-staged: runs on every *.ts/*.tsx commit - vitest: src/lib/no-hardcoded-colors.test.ts replaces old audit test - Allowlist: shadcn/ui primitives (button, badge, dialog) + 4 graphical files
This commit is contained in:
1 parent
0fa832d577
commit
58c35a2920
15 files changed
+276
-129
No files matched your search
+2
-1
@@ -27,7 +27,8 @@
|
|||||||
"test:housekeeping": "vitest run --coverage.enabled=false src/features/housekeeping src/lib/admin-theme-source-audit.test.ts src/lib/admin/authorization-contract.test.ts"
|
"test:housekeeping": "vitest run --coverage.enabled=false src/features/housekeeping src/lib/admin-theme-source-audit.test.ts src/lib/admin/authorization-contract.test.ts"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,jsx,tsx,json}": "biome check --write --no-errors-on-unmatched"
|
"*.{js,ts,jsx,tsx,json}": "biome check --write --no-errors-on-unmatched",
|
||||||
|
"*.{ts,tsx}": "node scripts/check-admin-colors.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "1.7.0",
|
"@base-ui/react": "1.7.0",
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -304,9 +304,9 @@ const GROUPS: Group[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const METHOD_CLASS: Record<Method, string> = {
|
const METHOD_CLASS: Record<Method, string> = {
|
||||||
GET: "bg-green-600/20 text-[var(--color-text)]",
|
GET: "bg-primary/20 text-[var(--color-text)]",
|
||||||
POST: "bg-[var(--color-primary)]/18 text-[var(--color-text)]",
|
POST: "bg-[var(--color-primary)]/18 text-[var(--color-text)]",
|
||||||
DELETE: "bg-red-500/22 text-[var(--color-text)]",
|
DELETE: "bg-destructive/22 text-[var(--color-text)]",
|
||||||
};
|
};
|
||||||
|
|
||||||
function AuthTag({ endpoint }: { endpoint: Endpoint }) {
|
function AuthTag({ endpoint }: { endpoint: Endpoint }) {
|
||||||
@@ -332,7 +332,7 @@ function AuthTag({ endpoint }: { endpoint: Endpoint }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-green-600/20 text-[var(--color-text)]"
|
className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-primary/20 text-[var(--color-text)]"
|
||||||
title="No authentication required"
|
title="No authentication required"
|
||||||
>
|
>
|
||||||
Public
|
Public
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ export default async function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||||
</span>
|
</span>
|
||||||
{th("online", { count: online, hotel: "" }).trim()}
|
{th("online", { count: online, hotel: "" }).trim()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -232,8 +232,8 @@ export default async function Home() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2 w-2">
|
<span className="relative flex h-2 w-2">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||||
</span>
|
</span>
|
||||||
{th("online", { count: online, hotel: hotelName })}
|
{th("online", { count: online, hotel: hotelName })}
|
||||||
</div>
|
</div>
|
||||||
@@ -454,8 +454,10 @@ export default async function Home() {
|
|||||||
sizes="(max-width: 640px) 100vw, 400px"
|
sizes="(max-width: 640px) 100vw, 400px"
|
||||||
className="rounded-xl object-cover transition-all duration-300 group-hover:scale-105"
|
className="rounded-xl object-cover transition-all duration-300 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-2.5 top-2.5 flex h-8 w-8 translate-x-1 items-center justify-center rounded-full bg-black/50 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
|
<div className="absolute right-2.5 top-2.5 flex h-8 w-8 translate-x-1 items-center justify-center rounded-full bg-foreground/50 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100">
|
||||||
<span className="text-sm font-bold text-white">→</span>
|
<span className="text-sm font-bold text-primary-foreground">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 w-full p-2.5 sm:p-3"
|
className="absolute bottom-0 left-0 w-full p-2.5 sm:p-3"
|
||||||
@@ -464,7 +466,7 @@ export default async function Home() {
|
|||||||
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 50%, transparent 100%)",
|
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.4) 50%, transparent 100%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3 className="truncate text-sm sm:text-base font-bold text-white text-shadow-sm">
|
<h3 className="truncate text-sm sm:text-base font-bold text-primary-foreground text-shadow-sm">
|
||||||
{a.title}
|
{a.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default async function RegisterPage() {
|
|||||||
"var(--color-primary-readable, var(--color-primary))",
|
"var(--color-primary-readable, var(--color-primary))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="w-2 h-2 rounded-full bg-green-500" />
|
<span className="w-2 h-2 rounded-full bg-primary" />
|
||||||
{tpr("usersOnline", { count: online })}
|
{tpr("usersOnline", { count: online })}
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<h1
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ function PrefixDialog({
|
|||||||
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
|
const hasMultiColor = colors.length > 1 && new Set(colors).size > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/50">
|
||||||
<div className="bg-background rounded-lg shadow-lg w-full max-w-md p-6 space-y-4">
|
<div className="bg-background rounded-lg shadow-lg w-full max-w-md p-6 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold">
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export function ClientView({
|
|||||||
// biome-ignore lint/a11y/noStaticElementInteractions: draggable toolbar with explicit mouse/touch handlers
|
// biome-ignore lint/a11y/noStaticElementInteractions: draggable toolbar with explicit mouse/touch handlers
|
||||||
<div
|
<div
|
||||||
ref={toolbarRef}
|
ref={toolbarRef}
|
||||||
className={`fixed z-[9999] backdrop-blur-xl bg-black/30 rounded-xl border border-white/10 shadow-2xl px-1.5 py-1 select-none ${dragging ? "cursor-grabbing" : "cursor-grab"}`}
|
className={`fixed z-[9999] backdrop-blur-xl bg-foreground/30 rounded-xl border border-white/10 shadow-2xl px-1.5 py-1 select-none ${dragging ? "cursor-grabbing" : "cursor-grab"}`}
|
||||||
style={{
|
style={{
|
||||||
top: pos.top,
|
top: pos.top,
|
||||||
left: pos.left,
|
left: pos.left,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function CatalogManagerDialog({
|
|||||||
{mounted && open
|
{mounted && open
|
||||||
? createPortal(
|
? createPortal(
|
||||||
<div className="fixed inset-0 z-[200]">
|
<div className="fixed inset-0 z-[200]">
|
||||||
<div className="absolute inset-0 bg-black/70" />
|
<div className="absolute inset-0 bg-foreground/70" />
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function MediaPicker({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label="Close media picker"
|
aria-label="Close media picker"
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/50"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) setOpen(false);
|
if (e.target === e.currentTarget) setOpen(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export function RegisterForm({
|
|||||||
>
|
>
|
||||||
{(error || state.error) && (
|
{(error || state.error) && (
|
||||||
<div
|
<div
|
||||||
className="animate-fade-in-up p-3 rounded-lg text-sm font-semibold text-white"
|
className="animate-fade-in-up p-3 rounded-lg text-sm font-semibold text-primary-foreground"
|
||||||
style={{ backgroundColor: "var(--color-danger)" }}
|
style={{ backgroundColor: "var(--color-danger)" }}
|
||||||
>
|
>
|
||||||
{error || state.error}
|
{error || state.error}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export async function SiteHeader() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1
|
<h1
|
||||||
className="font-black tracking-tight leading-none text-white"
|
className="font-black tracking-tight leading-none text-primary-foreground"
|
||||||
style={{
|
style={{
|
||||||
fontFamily: "var(--font-nunito)",
|
fontFamily: "var(--font-nunito)",
|
||||||
fontSize: "clamp(2rem, 7vw, 5.5rem)",
|
fontSize: "clamp(2rem, 7vw, 5.5rem)",
|
||||||
@@ -133,8 +133,8 @@ export async function SiteHeader() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="relative flex h-2 w-2 sm:h-2.5 sm:w-2.5">
|
<span className="relative flex h-2 w-2 sm:h-2.5 sm:w-2.5">
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 sm:h-2.5 sm:w-2.5 bg-green-500" />
|
<span className="relative inline-flex rounded-full h-2 w-2 sm:h-2.5 sm:w-2.5 bg-primary" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="text-xs sm:text-sm font-bold tracking-wide"
|
className="text-xs sm:text-sm font-bold tracking-wide"
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
|||||||
</form>
|
</form>
|
||||||
<Link
|
<Link
|
||||||
href="/messages"
|
href="/messages"
|
||||||
className="relative flex items-center gap-1 rounded-lg px-1.5 sm:px-2 py-1.5 transition-colors hover:bg-white/10"
|
className="relative flex items-center gap-1 rounded-lg px-1.5 sm:px-2 py-1.5 transition-colors hover:bg-background/10"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -376,7 +376,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
|
|||||||
<Link
|
<Link
|
||||||
key={u.id}
|
key={u.id}
|
||||||
href="/messages"
|
href="/messages"
|
||||||
className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-white/10"
|
className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs transition-colors hover:bg-background/10"
|
||||||
>
|
>
|
||||||
<UserAvatarThumbnail
|
<UserAvatarThumbnail
|
||||||
figure={u.look}
|
figure={u.look}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user