fix: isolate shared admin styling

This commit is contained in:
Simo committed 2026-07-12 18:50:44 +02:00
1 parent 4ce35e91fb
commit 0fe482009c
15 files changed
+157 -150

No files matched your search

+12 -12
View File
@@ -158,7 +158,7 @@ export default async function AdminLayout({ children }: { children: ReactNode })
}
return (
<div className="grid grid-cols-1 lg:grid-cols-[260px_minmax(0,1fr)] min-h-screen bg-[var(--color-background)]">
<div className="grid grid-cols-1 lg:grid-cols-[260px_minmax(0,1fr)] min-h-screen bg-[var(--admin-canvas)]">
<Sidebar staff={staff} />
<div className="flex flex-col min-w-0 p-5 lg:p-6">
<Topbar staff={staff} />
@@ -175,9 +175,9 @@ async function Sidebar({ staff }: { staff: { id: number; username: string; rank:
return (
<aside className="sticky top-0 self-start h-screen bg-[var(--admin-sidebar-background)] text-[var(--admin-sidebar-text-readable)] shadow-xl flex flex-col">
<div className="flex items-center gap-3 px-4 py-5 border-b border-[var(--border-subtle)]">
<div className="flex items-center gap-3 px-4 py-5 border-b border-[var(--admin-border)]">
<span
className="flex-none w-10 h-10 rounded-xl grid place-items-center font-extrabold text-base text-[var(--color-primary-foreground-readable)] bg-[var(--color-primary)] shadow-lg shadow-[var(--color-primary)]/20"
className="flex-none w-10 h-10 rounded-xl grid place-items-center font-extrabold text-base text-[var(--color-primary-foreground-readable)] bg-[var(--admin-accent)] shadow-lg shadow-[var(--admin-accent)]/20"
aria-hidden
>
{initial}
@@ -208,10 +208,10 @@ async function Sidebar({ staff }: { staff: { id: number; username: string; rank:
))}
</nav>
<div className="px-3 py-3 border-t border-[var(--border-subtle)]">
<div className="px-3 py-3 border-t border-[var(--admin-border)]">
<Link
href="/"
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[var(--admin-sidebar-muted-readable)] text-xs font-medium hover:text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--color-primary)]/10 transition-all duration-150 no-underline"
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-[var(--admin-sidebar-muted-readable)] text-xs font-medium hover:text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--admin-accent)]/10 transition-all duration-150 no-underline"
>
<LogOut size={14} />
<span>{t("backToSite")}</span>
@@ -225,19 +225,19 @@ async function Topbar({ staff }: { staff: { id: number; username: string; rank:
const t = await getTranslations("pages.admin.nav");
return (
<header className="flex items-center justify-between gap-4 bg-[var(--color-surface)] border border-[var(--border-subtle)] rounded-2xl px-5 py-3 mb-5 shadow-card">
<header className="flex items-center justify-between gap-4 bg-[var(--admin-surface)] border border-[var(--admin-border)] rounded-2xl px-5 py-3 mb-5 shadow-card">
<div className="flex items-center gap-3">
<h2 className="m-0 text-base font-extrabold text-[var(--color-text)]">{t("housekeeping")}</h2>
<span className="hidden sm:inline-block w-px h-5 bg-[var(--border-subtle)]" />
<span className="hidden sm:inline text-xs text-[var(--color-text-muted)]">
<h2 className="m-0 text-base font-extrabold text-[var(--admin-text)]">{t("housekeeping")}</h2>
<span className="hidden sm:inline-block w-px h-5 bg-[var(--admin-border)]" />
<span className="hidden sm:inline text-xs text-[var(--admin-text-muted)]">
{t("hotelManagementPanel")}
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-[var(--color-text-muted)]">
<strong className="text-[var(--color-text)]">{staff.username}</strong>
<span className="text-xs text-[var(--admin-text-muted)]">
<strong className="text-[var(--admin-text)]">{staff.username}</strong>
</span>
<span className="inline-flex items-center gap-1.5 text-[0.6rem] font-bold uppercase tracking-wider text-[var(--color-primary-foreground-readable)] bg-[var(--color-primary)] rounded-full px-2.5 py-1">
<span className="inline-flex items-center gap-1.5 text-[0.6rem] font-bold uppercase tracking-wider text-[var(--color-primary-foreground-readable)] bg-[var(--admin-accent)] rounded-full px-2.5 py-1">
{t("rankLabel", { rank: staff.rank })}
</span>
</div>
+66 -66
View File
@@ -437,9 +437,9 @@ tbody tr:hover {
/* Cards used inside admin pages */
.admin-card {
background: var(--color-navbar);
color: var(--color-navbar-text-readable, var(--color-navbar-text));
border: 1px solid var(--border-subtle);
background: var(--admin-surface);
color: var(--admin-text);
border: 1px solid var(--admin-border);
border-radius: 16px;
box-shadow: var(--shadow-card);
padding: 1.25rem;
@@ -472,8 +472,8 @@ tbody tr:hover {
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.75rem;
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-navbar));
border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
background: color-mix(in srgb, var(--admin-accent) 8%, var(--admin-surface));
border: 1px solid color-mix(in srgb, var(--admin-accent) 15%, transparent);
border-radius: 999px;
font-size: 0.78rem;
font-weight: 600;
@@ -482,8 +482,8 @@ tbody tr:hover {
/* Tables */
.admin-page table {
width: 100%;
background: var(--color-navbar);
border: 1px solid var(--border-subtle);
background: var(--admin-surface);
border: 1px solid var(--admin-border);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow-card);
@@ -491,9 +491,9 @@ tbody tr:hover {
border-spacing: 0;
}
.admin-page thead th {
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-navbar));
color: var(--color-text-muted);
border-bottom: 1px solid var(--border-subtle);
background: color-mix(in srgb, var(--admin-accent) 8%, var(--admin-surface));
color: var(--admin-text-muted);
border-bottom: 1px solid var(--admin-border);
white-space: nowrap;
font-size: 0.68rem;
font-weight: 700;
@@ -503,14 +503,14 @@ tbody tr:hover {
}
.admin-page td {
padding: 0.7rem 0.9rem;
border-top: 1px solid color-mix(in srgb, var(--color-text-muted) 8%, transparent);
border-top: 1px solid color-mix(in srgb, var(--admin-text-muted) 8%, transparent);
font-size: 0.88rem;
}
.admin-page tbody tr {
transition: background 0.12s ease;
}
.admin-page tbody tr:hover {
background: color-mix(in srgb, var(--color-primary) 4%, transparent);
background: color-mix(in srgb, var(--admin-accent) 4%, transparent);
}
.admin-page thead tr:first-child th:first-child {
border-radius: 16px 0 0 0;
@@ -524,7 +524,7 @@ tbody tr:hover {
font-size: 0.9rem;
font-weight: 700;
margin: 1.5rem 0 0.75rem;
color: var(--color-text);
color: var(--admin-text);
}
.admin-section-title:first-child {
margin-top: 0;
@@ -540,7 +540,7 @@ tbody tr:hover {
.admin-empty {
text-align: center;
padding: 2.5rem 1rem;
color: var(--color-text-muted);
color: var(--admin-text-muted);
font-size: 0.85rem;
}
/* ── Public design system (faithful to the atom theme) ──────────
@@ -550,9 +550,9 @@ tbody tr:hover {
display: flex;
flex-direction: column;
width: 100%;
background: var(--color-navbar);
color: var(--color-navbar-text-readable, var(--color-navbar-text));
border: 1px solid color-mix(in srgb, var(--color-text-muted) 12%, transparent);
background: var(--admin-surface);
color: var(--admin-text);
border: 1px solid color-mix(in srgb, var(--admin-text-muted) 12%, transparent);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-card);
overflow: hidden;
@@ -568,10 +568,10 @@ tbody tr:hover {
padding: 0.9rem 1.1rem;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--gradient-from) 10%, var(--color-navbar)),
color-mix(in srgb, var(--gradient-to) 5%, var(--color-navbar))
color-mix(in srgb, var(--admin-accent) 10%, var(--admin-surface)),
color-mix(in srgb, var(--admin-accent) 5%, var(--admin-surface))
);
border-bottom: 1px solid color-mix(in srgb, var(--color-text-muted) 8%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--admin-text-muted) 8%, transparent);
}
.content-card-icon {
flex: none;
@@ -583,10 +583,10 @@ tbody tr:hover {
font-size: 1.25rem;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--gradient-from) 22%, var(--color-navbar)),
color-mix(in srgb, var(--gradient-to) 12%, var(--color-navbar))
color-mix(in srgb, var(--admin-accent) 22%, var(--admin-surface)),
color-mix(in srgb, var(--admin-accent) 12%, var(--admin-surface))
);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 30%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--admin-accent) 30%, transparent);
}
.content-card-head-text {
min-width: 0;
@@ -597,12 +597,12 @@ tbody tr:hover {
font-weight: 700;
font-size: 1rem;
line-height: 1.3;
color: var(--color-navbar-text-readable, var(--color-navbar-text));
color: var(--admin-text);
}
.content-card-subtitle {
margin: 0.15rem 0 0;
font-size: 0.78rem;
color: var(--color-text-muted-readable, var(--color-text-muted));
color: var(--admin-text-muted);
}
.content-card-action {
flex: none;
@@ -646,8 +646,8 @@ tbody tr:hover {
gap: 0.85rem;
}
.stat-block {
background: var(--color-background);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 12%, transparent);
background: var(--admin-canvas);
border: 1px solid color-mix(in srgb, var(--admin-text-muted) 12%, transparent);
border-radius: 12px;
padding: 0.85rem 1rem;
text-align: center;
@@ -656,12 +656,12 @@ tbody tr:hover {
font-size: 1.6rem;
font-weight: 800;
line-height: 1.1;
color: var(--color-primary-readable, var(--color-primary));
color: var(--admin-accent-text);
}
.stat-block-label {
margin-top: 0.2rem;
font-size: 0.78rem;
color: var(--color-text-muted-readable, var(--color-text-muted));
color: var(--admin-text-muted);
}
/* Empty states */
@@ -683,12 +683,12 @@ tbody tr:hover {
font-weight: 700;
padding: 0.1rem 0.55rem 0.1rem 0.45rem;
border-radius: 999px;
background: color-mix(in srgb, var(--color-text-muted) 14%, transparent);
color: var(--color-text-muted-readable, var(--color-text-muted));
background: color-mix(in srgb, var(--admin-text-muted) 14%, transparent);
color: var(--admin-text-muted);
}
.online-badge.online {
background: color-mix(in srgb, var(--color-success) 16%, transparent);
color: var(--color-success);
background: color-mix(in srgb, var(--admin-success) 16%, transparent);
color: var(--admin-success);
}
.online-dot {
width: 8px;
@@ -697,8 +697,8 @@ tbody tr:hover {
background: currentColor;
}
.online-badge.online .online-dot {
background: var(--color-success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 22%, transparent);
background: var(--admin-success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--admin-success) 22%, transparent);
}
/* Leaderboard rank chip */
@@ -711,12 +711,12 @@ tbody tr:hover {
border-radius: 999px;
font-weight: 800;
font-size: 0.95rem;
background: var(--color-background);
border: 1px solid color-mix(in srgb, var(--color-text-muted) 18%, transparent);
background: var(--admin-canvas);
border: 1px solid color-mix(in srgb, var(--admin-text-muted) 18%, transparent);
}
.rank-badge.rank-1 {
background: color-mix(in srgb, var(--color-warning) 20%, transparent);
border-color: var(--color-warning);
background: color-mix(in srgb, var(--admin-warning) 20%, transparent);
border-color: var(--admin-warning);
}
.rank-badge.rank-2 {
background: color-mix(in srgb, #94a3b8 22%, transparent);
@@ -743,7 +743,7 @@ tbody tr:hover {
gap: 0.25rem;
}
.radio-sidebar-link:hover {
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
background: color-mix(in srgb, var(--admin-accent) 10%, transparent);
text-decoration: none;
}
@media (max-width: 48rem) {
@@ -757,7 +757,7 @@ tbody tr:hover {
flex-wrap: wrap;
gap: 0.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--admin-text-muted) 14%, transparent);
overflow-x: auto;
}
.radio-sidebar-head {
@@ -788,7 +788,7 @@ tbody tr:hover {
appearance: none;
border: 0;
background: transparent;
color: var(--link-color-readable, var(--link-color));
color: var(--admin-accent-text);
cursor: pointer;
font: inherit;
font-size: 0.78rem;
@@ -801,11 +801,11 @@ tbody tr:hover {
}
.cms-info-trigger:hover,
.cms-info-trigger:focus-visible {
color: var(--link-hover-color-readable, var(--link-hover-color));
color: var(--admin-accent-text);
}
.cms-info-trigger:focus-visible,
.cms-info-close:focus-visible {
outline: 3px solid color-mix(in srgb, var(--color-primary) 42%, transparent);
outline: 3px solid color-mix(in srgb, var(--admin-accent) 42%, transparent);
outline-offset: 3px;
}
.cms-info-backdrop {
@@ -821,13 +821,13 @@ tbody tr:hover {
.cms-info-dialog {
position: relative;
width: min(440px, 100%);
border: 1px solid color-mix(in srgb, var(--color-primary) 30%, var(--border));
border: 1px solid color-mix(in srgb, var(--admin-accent) 30%, var(--admin-border));
border-radius: 8px;
background:
linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 12%, transparent), transparent 42%),
var(--color-surface);
linear-gradient(135deg, color-mix(in srgb, var(--admin-accent) 12%, transparent), transparent 42%),
var(--admin-surface);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.34);
color: var(--color-text-readable, var(--color-text));
color: var(--admin-text);
padding: 1.25rem;
}
.cms-info-close {
@@ -838,20 +838,20 @@ tbody tr:hover {
width: 2rem;
height: 2rem;
place-items: center;
border: 1px solid color-mix(in srgb, var(--color-text-muted) 18%, transparent);
border: 1px solid color-mix(in srgb, var(--admin-text-muted) 18%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--color-surface) 84%, transparent);
color: var(--color-text-muted-readable, var(--color-text-muted));
background: color-mix(in srgb, var(--admin-surface) 84%, transparent);
color: var(--admin-text-muted);
cursor: pointer;
font-weight: 900;
line-height: 1;
}
.cms-info-close:hover {
color: var(--color-text-readable, var(--color-text));
border-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
color: var(--admin-text);
border-color: color-mix(in srgb, var(--admin-accent) 40%, transparent);
}
.cms-info-kicker {
color: var(--color-primary-readable, var(--color-primary));
color: var(--admin-accent-text);
font-size: 0.74rem;
font-weight: 900;
letter-spacing: 0;
@@ -867,17 +867,17 @@ tbody tr:hover {
display: inline-flex;
align-items: center;
margin-top: 0.65rem;
border: 1px solid color-mix(in srgb, var(--color-primary) 34%, transparent);
border: 1px solid color-mix(in srgb, var(--admin-accent) 34%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--color-primary) 14%, transparent);
color: var(--color-text-readable, var(--color-text));
background: color-mix(in srgb, var(--admin-accent) 14%, transparent);
color: var(--admin-text);
font-size: 0.75rem;
font-weight: 900;
padding: 0.3rem 0.6rem;
}
.cms-info-copy {
margin: 0.9rem 0 0;
color: var(--color-text-muted-readable, var(--color-text-muted));
color: var(--admin-text-muted);
font-size: 0.92rem;
line-height: 1.55;
}
@@ -888,20 +888,20 @@ tbody tr:hover {
margin: 1rem 0 0;
}
.cms-info-grid div {
border: 1px solid color-mix(in srgb, var(--color-text-muted) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--admin-text-muted) 14%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-background) 68%, transparent);
background: color-mix(in srgb, var(--admin-canvas) 68%, transparent);
padding: 0.7rem;
}
.cms-info-grid dt {
color: var(--color-text-muted-readable, var(--color-text-muted));
color: var(--admin-text-muted);
font-size: 0.68rem;
font-weight: 800;
text-transform: uppercase;
}
.cms-info-grid dd {
margin: 0.18rem 0 0;
color: var(--color-text-readable, var(--color-text));
color: var(--admin-text);
font-size: 0.8rem;
font-weight: 900;
line-height: 1.25;
@@ -916,17 +916,17 @@ tbody tr:hover {
align-items: center;
justify-content: space-between;
gap: 1rem;
border: 1px solid color-mix(in srgb, var(--color-primary) 18%, transparent);
border: 1px solid color-mix(in srgb, var(--admin-accent) 18%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-primary) 8%, transparent);
background: color-mix(in srgb, var(--admin-accent) 8%, transparent);
padding: 0.65rem 0.75rem;
}
.cms-info-credit span {
color: var(--color-text-readable, var(--color-text));
color: var(--admin-text);
font-weight: 900;
}
.cms-info-credit small {
color: var(--color-text-muted-readable, var(--color-text-muted));
color: var(--admin-text-muted);
font-size: 0.74rem;
font-weight: 700;
text-align: right;
+2 -2
View File
@@ -27,8 +27,8 @@ 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-[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"
? "bg-[var(--admin-accent)]/15 text-[var(--admin-sidebar-text-readable)] shadow-sm border-l-2 border-[var(--admin-accent)]"
: "text-[var(--admin-sidebar-muted-readable)] hover:text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--admin-accent)]/10 hover:no-underline"
}`}
>
{icon ? (
@@ -287,7 +287,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
{isDirty && (
<Badge
variant="outline"
className="h-4 px-1.5 text-[9px] text-[var(--color-warning-text-readable)] border-[var(--color-warning-border)] bg-[var(--color-warning-subtle)]"
className="h-4 px-1.5 text-[9px] text-[var(--admin-warning)] border-[var(--admin-warning-border)] bg-[var(--admin-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-[var(--color-warning-subtle)] border-b border-[var(--color-warning-border)] text-xs text-[var(--color-warning-text-readable)] shrink-0">
<div className="flex items-center gap-2 px-4 py-1.5 bg-[var(--admin-warning-subtle)] border-b border-[var(--admin-warning-border)] text-xs text-[var(--admin-warning)] 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-[var(--color-warning)]" />}
{isDirty && <span className="ml-0.5 h-1.5 w-1.5 rounded-full bg-[var(--admin-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-[var(--color-warning)] text-[var(--color-warning-foreground-readable)] text-[9px] px-1 py-0.5 leading-none font-medium">
<span className="ml-0.5 rounded-full bg-[var(--admin-warning)] text-[var(--admin-warning-foreground)] text-[9px] px-1 py-0.5 leading-none font-medium">
{missing}
</span>
) : null
@@ -127,7 +127,7 @@ export const TreeItemContent = React.memo(
!sortMode && 'cursor-pointer',
isSelected && !isDragging && 'bg-primary/10 ring-1 ring-primary/30',
isMultiSelected && !isSelected && !isDragging && 'bg-primary/5 ring-1 ring-primary/15',
isFocused && !isSelected && !isMultiSelected && 'ring-2 ring-blue-500/40',
isFocused && !isSelected && !isMultiSelected && 'ring-2 ring-[var(--admin-focus-ring)]/40',
!isSelected && !isMultiSelected && !isFocused && !isDragging && 'hover:bg-accent/60',
isAncestorOfSelected &&
!isSelected &&
+16 -16
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-[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-[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)]',
default_3x3: 'bg-[var(--admin-info-subtle)] text-[var(--admin-info)] dark:text-[var(--admin-info)]',
frontpage: 'bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)] dark:text-[var(--admin-warning)]',
spaces_new: 'bg-[var(--admin-success-subtle)] text-[var(--admin-success)] dark:text-[var(--admin-success)]',
pets: 'bg-[var(--admin-error)]/15 text-[var(--admin-error)]',
pets2: 'bg-[var(--admin-error)]/15 text-[var(--admin-error)]',
pets3: 'bg-[var(--admin-error)]/15 text-[var(--admin-error)]',
marketplace: 'bg-[var(--admin-accent)]/15 text-[var(--admin-accent-text)]',
recycler: 'bg-[var(--admin-success-subtle)] text-[var(--admin-success)] dark:text-[var(--admin-success)]',
club_buy: 'bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)] dark:text-[var(--admin-warning)]',
single_bundle: 'bg-[var(--admin-info-subtle)] text-[var(--admin-info)] dark:text-[var(--admin-info)]',
room_bundle: 'bg-[var(--admin-info-subtle)] text-[var(--admin-info)] dark:text-[var(--admin-info)]',
}
/* ─── Highlight matched text ─────────────────────────────── */
@@ -131,7 +131,7 @@ function HighlightText({ text, query }: { text: string; query: string }) {
return (
<>
{text.slice(0, idx)}
<mark className="bg-[var(--color-warning-subtle)] text-[var(--color-warning-text-readable)] rounded-sm px-0.5">
<mark className="bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)] 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-[var(--color-success-text-readable)] dark:text-[var(--color-success-text-readable)]'
? 'bg-background shadow-sm text-[var(--admin-success)] dark:text-[var(--admin-success)]'
: '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-[var(--color-error-text-readable)] dark:text-[var(--color-error-text-readable)]'
? 'bg-background shadow-sm text-[var(--admin-error)] dark:text-[var(--admin-error)]'
: '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-[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)]',
? 'hover:bg-[var(--admin-success-subtle)] text-muted-foreground hover:text-[var(--admin-success)]'
: 'hover:bg-[var(--admin-error-subtle)] text-muted-foreground hover:text-[var(--admin-error)]',
)}
>
{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-[var(--color-success-text-readable)]">OK</span>
<span className="text-[var(--admin-success)]">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-[var(--color-warning-subtle)] px-1 text-[8px] text-white">
<Badge className="h-4 gap-0.5 border-0 bg-[var(--admin-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-[var(--color-warning-text-readable)] dark:text-[var(--color-warning-text-readable)]">
<span className="text-[var(--admin-warning)] dark:text-[var(--admin-warning)]">
{item.costCredits}c
</span>
)}
+28 -28
View File
@@ -29,20 +29,20 @@ export function StatusCard({
icon?: ReactNode;
}) {
return (
<div className="bg-[var(--color-surface)] border border-[var(--border-subtle)] rounded-2xl p-5 shadow-card hover:shadow-card-hover transition-shadow duration-200">
<div className="bg-[var(--admin-surface)] border border-[var(--admin-border)] rounded-2xl p-5 shadow-card hover:shadow-card-hover transition-shadow duration-200">
<div className="flex items-center justify-between gap-3 mb-3">
<span className="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--color-text-muted)]">
<span className="text-[0.65rem] font-bold uppercase tracking-widest text-[var(--admin-text-muted)]">
{label}
</span>
{icon ? (
<span className="w-8 h-8 rounded-xl bg-gradient-to-br from-[var(--color-primary)]/15 to-[var(--color-primary)]/5 grid place-items-center text-[var(--color-primary)] flex-none">
<span className="w-8 h-8 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/15 to-[var(--admin-accent)]/5 grid place-items-center text-[var(--admin-accent)] flex-none">
{icon}
</span>
) : null}
</div>
<div className="text-2xl font-extrabold text-[var(--color-text)] leading-tight">{value}</div>
<div className="text-2xl font-extrabold text-[var(--admin-text)] leading-tight">{value}</div>
{hint ? (
<div className="text-[0.7rem] text-[var(--color-text-muted)] mt-1.5 font-medium">{hint}</div>
<div className="text-[0.7rem] text-[var(--admin-text-muted)] mt-1.5 font-medium">{hint}</div>
) : null}
</div>
);
@@ -61,24 +61,24 @@ export function DiagnosticRow({
const text = state === "ok" ? "OK" : state === "warn" ? "WARN" : "ERROR";
const dotColor =
state === "ok"
? "bg-[var(--color-success-subtle)] shadow-[0_0_0_3px_rgba(22,163,74,0.22)]"
? "bg-[var(--admin-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-[var(--color-error-subtle)] shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
? "bg-[var(--admin-accent)] shadow-[0_0_0_3px_rgba(238,180,37,0.22)]"
: "bg-[var(--admin-error-subtle)] shadow-[0_0_0_3px_rgba(239,68,68,0.22)]";
const badgeColor =
state === "ok"
? "bg-[var(--color-success-subtle)]"
? "bg-[var(--admin-success-subtle)]"
: state === "danger"
? "bg-[var(--color-error-subtle)]"
: "bg-[var(--color-primary)]/18";
? "bg-[var(--admin-error-subtle)]"
: "bg-[var(--admin-accent)]/18";
return (
<div className="flex items-center gap-2.5 py-2 px-0.5 border-t border-[var(--border-subtle)] first:border-t-0">
<div className="flex items-center gap-2.5 py-2 px-0.5 border-t border-[var(--admin-border)] first:border-t-0">
<span className={`flex-none w-[9px] h-[9px] rounded-full ${dotColor}`} aria-hidden />
<span className="font-bold text-sm">{label}</span>
<span className="flex-1 text-[0.82rem] text-[var(--color-text-muted)]">{detail}</span>
<span className="flex-1 text-[0.82rem] text-[var(--admin-text-muted)]">{detail}</span>
<span
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${badgeColor} text-[var(--color-text)]`}
className={`inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full ${badgeColor} text-[var(--admin-text)]`}
>
{text}
</span>
@@ -90,7 +90,7 @@ export function DiagnosticRow({
export function InfoItem({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="flex flex-col gap-0.5 text-sm">
<span className="text-[var(--color-text-muted)]">{label}</span>
<span className="text-[var(--admin-text-muted)]">{label}</span>
<strong>{value}</strong>
</div>
);
@@ -115,54 +115,54 @@ export function OnlineUsersWidget({
};
return (
<div className="bg-[var(--color-surface)] rounded-xl border border-[var(--border-subtle)] p-6">
<div className="bg-[var(--admin-surface)] rounded-xl border border-[var(--admin-border)] p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="m-0 text-lg font-semibold text-[var(--color-text)]">Online Now</h3>
<span className="text-sm text-[var(--color-text-muted)] bg-[var(--color-primary)]/10 px-3 py-1 rounded-full">
<h3 className="m-0 text-lg font-semibold text-[var(--admin-text)]">Online Now</h3>
<span className="text-sm text-[var(--admin-text-muted)] bg-[var(--admin-accent)]/10 px-3 py-1 rounded-full">
{isLoading ? "..." : users.length} users
</span>
</div>
<div className="max-h-80 overflow-y-auto flex flex-col gap-3">
{users.length === 0 ? (
<div className="py-8 text-center bg-[var(--color-text-muted)]/5 rounded-lg">
<span className="text-[var(--color-text-muted)] text-sm">No users online</span>
<div className="py-8 text-center bg-[var(--admin-text-muted)]/5 rounded-lg">
<span className="text-[var(--admin-text-muted)] text-sm">No users online</span>
</div>
) : (
users.map((user) => (
<div
key={user.id}
className="flex items-center gap-3 p-3 bg-[var(--color-text-muted)]/5 rounded-lg transition-all duration-150 hover:bg-[var(--color-text-muted)]/10 hover:translate-x-0.5"
className="flex items-center gap-3 p-3 bg-[var(--admin-text-muted)]/5 rounded-lg transition-all duration-150 hover:bg-[var(--admin-text-muted)]/10 hover:translate-x-0.5"
>
<div className="relative flex-none w-[50px] h-[62px]">
<Image
src={`https://www.habbo.com/habbo-imaging/avatarimage?width=50&height=62&direction=2&headonly=1&crop=face&action=wav&gender=m&look=${encodeURIComponent(user.look)}`}
alt={user.username}
fill
className="rounded object-cover border border-[var(--border-subtle)]"
className="rounded object-cover border border-[var(--admin-border)]"
unoptimized
/>
{user.roomId && user.roomName && (
<div className="absolute -bottom-0.5 -right-0.5">
<span className="block w-[10px] h-[10px] bg-[var(--color-success-subtle)] rounded-full border-2 border-[var(--color-surface)]" />
<span className="block w-[10px] h-[10px] bg-[var(--admin-success-subtle)] rounded-full border-2 border-[var(--admin-surface)]" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<div className="font-medium text-[var(--color-text)] mb-1 truncate">
<div className="font-medium text-[var(--admin-text)] mb-1 truncate">
<a
href={`/u/${user.username}`}
className="text-inherit no-underline hover:text-[var(--color-primary)] hover:underline"
className="text-inherit no-underline hover:text-[var(--admin-accent)] hover:underline"
>
{user.username}
</a>
</div>
<div className="text-[0.75rem] text-[var(--color-text-muted)]">
<div className="text-[0.75rem] text-[var(--admin-text-muted)]">
{user.roomId && user.roomName ? (
<span className="text-[var(--color-success-text-readable)] font-medium">🏠 {user.roomName}</span>
<span className="text-[var(--admin-success)] font-medium">🏠 {user.roomName}</span>
) : (
<span className="text-[var(--color-error-text-readable)]">⏱️ {formatLastSeen(user.lastActivity)}</span>
<span className="text-[var(--admin-error)]">⏱️ {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-[var(--color-warning-subtle)] dark:bg-[var(--color-warning-subtle)]' : ''
isChanged ? 'bg-[var(--admin-warning-subtle)] dark:bg-[var(--admin-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-[var(--color-warning-border)]' : ''
isChanged ? 'border-[var(--admin-warning-border)]' : ''
}`}
/>
{isChanged && (
+4 -4
View File
@@ -93,9 +93,9 @@ export function AdminMediaGrid() {
{progress.current} / {progress.total}
</span>
</div>
<div className="h-2 w-full bg-[var(--color-primary)]/10 rounded-full overflow-hidden">
<div className="h-2 w-full bg-[var(--admin-accent)]/10 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-hover)] transition-all duration-300"
className="h-full bg-gradient-to-r from-[var(--admin-accent)] to-[var(--admin-accent)] transition-all duration-300"
style={{ width: `${(progress.current / progress.total) * 100}%` }}
/>
</div>
@@ -111,12 +111,12 @@ export function AdminMediaGrid() {
{files.map((f) => (
<div
key={f.name}
className="border border-[var(--color-text-muted)]/14 rounded-[10px] overflow-hidden bg-[var(--color-background)]"
className="border border-[var(--admin-text-muted)]/14 rounded-[10px] overflow-hidden bg-[var(--admin-canvas)]"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={validImageUrl(f.url)} alt={f.name} className="w-full h-[120px] object-cover block" />
<div className="p-2">
<p className="text-xs text-[var(--color-text-muted)] m-0 mb-1 overflow-hidden text-ellipsis whitespace-nowrap">
<p className="text-xs text-[var(--admin-text-muted)] m-0 mb-1 overflow-hidden text-ellipsis whitespace-nowrap">
{f.name}
</p>
<button
+8 -8
View File
@@ -70,7 +70,7 @@ export function MediaPicker({ onSelect, current }: { onSelect: (url: string) =>
<span
style={{
fontSize: "0.78rem",
color: "var(--color-text-muted)",
color: "var(--admin-text-muted)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
@@ -89,9 +89,9 @@ export function MediaPicker({ onSelect, current }: { onSelect: (url: string) =>
>
<div
onClick={(e) => e.stopPropagation()}
className="bg-[var(--color-surface)] text-[var(--color-text)] rounded-2xl w-[min(90vw,680px)] max-h-[80vh] flex flex-col shadow-xl"
className="bg-[var(--admin-surface)] text-[var(--admin-text)] rounded-2xl w-[min(90vw,680px)] max-h-[80vh] flex flex-col shadow-xl"
>
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--color-text-muted)]/14">
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--admin-text-muted)]/14">
<strong className="text-base">Media Library</strong>
<div className="flex gap-2 items-center">
<label className="btn btn-primary text-sm cursor-pointer">
@@ -112,9 +112,9 @@ export function MediaPicker({ onSelect, current }: { onSelect: (url: string) =>
</div>
<div className="flex-1 overflow-auto p-3">
{loading ? (
<p className="text-xs text-[var(--color-text-muted)] text-center py-8">Loading…</p>
<p className="text-xs text-[var(--admin-text-muted)] text-center py-8">Loading…</p>
) : files.length === 0 ? (
<p className="text-xs text-[var(--color-text-muted)] text-center py-8">
<p className="text-xs text-[var(--admin-text-muted)] text-center py-8">
No images yet. Upload one above.
</p>
) : (
@@ -127,12 +127,12 @@ export function MediaPicker({ onSelect, current }: { onSelect: (url: string) =>
onSelect(f.url);
setOpen(false);
}}
className="border-2 border-transparent rounded-xl overflow-hidden cursor-pointer bg-[var(--color-background)] p-0 flex flex-col transition-all duration-150"
style={{ borderColor: f.url === current ? "var(--color-primary)" : "transparent" }}
className="border-2 border-transparent rounded-xl overflow-hidden cursor-pointer bg-[var(--admin-canvas)] p-0 flex flex-col transition-all duration-150"
style={{ borderColor: f.url === current ? "var(--admin-accent)" : "transparent" }}
onMouseEnter={(e) => {
if (f.url !== current)
e.currentTarget.style.borderColor =
"color-mix(in srgb, var(--color-primary) 40%, transparent)";
"color-mix(in srgb, var(--admin-accent) 40%, transparent)";
}}
onMouseLeave={(e) => {
if (f.url !== current) e.currentTarget.style.borderColor = "transparent";
+4 -4
View File
@@ -9,12 +9,12 @@ interface AdminPageHeaderProps {
export function AdminPageHeader({ icon: Icon, title, subtitle }: AdminPageHeaderProps) {
return (
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--color-primary)]/20 to-[var(--color-primary)]/5 grid place-items-center">
<Icon size={20} className="text-[var(--color-primary)]" />
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-[var(--admin-accent)]/20 to-[var(--admin-accent)]/5 grid place-items-center">
<Icon size={20} className="text-[var(--admin-accent)]" />
</div>
<div>
<h1 className="m-0 text-xl font-extrabold text-[var(--color-text)]">{title}</h1>
<p className="m-0 text-xs text-[var(--color-text-muted)] mt-0.5">{subtitle}</p>
<h1 className="m-0 text-xl font-extrabold text-[var(--admin-text)]">{title}</h1>
<p className="m-0 text-xs text-[var(--admin-text-muted)] mt-0.5">{subtitle}</p>
</div>
</div>
);
+2
View File
@@ -23,6 +23,8 @@ describe("themePaletteCss", () => {
"--admin-accent", "--admin-accent-foreground", "--admin-overlay",
"--admin-focus-ring", "--admin-success", "--admin-warning",
"--admin-error", "--admin-info",
"--admin-success-subtle", "--admin-warning-subtle",
"--admin-error-border", "--admin-info-foreground",
]) expect(css).toContain(variable);
});
});
+5
View File
@@ -65,6 +65,11 @@ export function themePaletteCss(selector: string, palette: ThemePalette): string
`--admin-sidebar-text:${admin.sidebarText};`,
`--admin-overlay:${admin.overlay};`,
`--admin-focus-ring:${admin.focusRing};`,
...(["success", "warning", "error", "info"] as const).flatMap((status) => [
`--admin-${status}-foreground:${readableColor("#ffffff", [admin[status]])};`,
`--admin-${status}-subtle:color-mix(in srgb, ${admin[status]} 14%, ${admin.surface});`,
`--admin-${status}-border:color-mix(in srgb, ${admin[status]} 38%, transparent);`,
]),
...(["success", "warning", "error", "info"] as const).flatMap((status) => [
`--color-${status}-text-readable:${semantic.byName[`${status}-text`]};`,
`--color-${status}-foreground-readable:${semantic.byName[`${status}-solid`]};`,