refactor: improve admin panel styling with new CSS helpers and page layout patterns
- Add .admin-card, .admin-filter-bar, .admin-stat-row, .admin-stat-chip, .admin-section-title, .admin-info-grid, .admin-empty CSS helpers - Improve admin table styling (uppercase headers, better spacing, border-separate) - Add dark mode support for admin tables and cards - Update housekeeping and settings pages to use new layout components - Clean up inline styles in housekeeping and settings pages
This commit is contained in:
1 parent
1ea953fd65
commit
b621ec79a3
3 files changed
+162
-82
No files matched your search
@@ -44,83 +44,76 @@ export default async function AdminHousekeeping({
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Housekeeping Permissions</h1>
|
||||
<p className="muted" style={{ marginTop: "-0.4rem" }}>
|
||||
Minimum rank required to access each housekeeping permission key. Saving an
|
||||
existing key overwrites its min rank and description.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Permissions" value={permissions.length} icon="🔑" />
|
||||
<StatusCard label="Highest min rank" value={maxRank} icon="⬆️" />
|
||||
<StatusCard label="Total min rank" value={totalMin} icon="∑" />
|
||||
</div>
|
||||
|
||||
<div className="card mt-6" style={{ marginBottom: "1rem" }}>
|
||||
<div style={{ display: "flex", gap: "0.5rem", alignItems: "center", flexWrap: "wrap" }}>
|
||||
<form style={{ display: "flex", gap: "0.5rem", flex: 1 }}>
|
||||
<input
|
||||
name="q"
|
||||
defaultValue={q}
|
||||
placeholder="Search permission key..."
|
||||
style={{ flex: 1, minWidth: 220 }}
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary">Search</button>
|
||||
</form>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<Link href="/admin/housekeeping?mode=manage" className="btn btn-outline">
|
||||
Manage
|
||||
</Link>
|
||||
<Link href="/admin/housekeeping?mode=import&form=selected_ids" className="btn btn-outline">
|
||||
Import selection
|
||||
</Link>
|
||||
</div>
|
||||
<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">
|
||||
<span className="text-lg">🔑</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="m-0 text-xl font-extrabold text-[var(--color-text)]">Housekeeping Permissions</h1>
|
||||
<p className="m-0 text-xs text-[var(--color-text-muted)] mt-0.5">
|
||||
Minimum rank required to access each housekeeping permission key.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-stat-row">
|
||||
<span className="admin-stat-chip">{permissions.length} permissions</span>
|
||||
<span className="admin-stat-chip">Highest min rank: {maxRank}</span>
|
||||
<span className="admin-stat-chip">Total min rank: {totalMin}</span>
|
||||
</div>
|
||||
|
||||
<div className="admin-filter-bar">
|
||||
<form className="flex gap-2 flex-1">
|
||||
<input name="q" defaultValue={q} placeholder="Search permission key..." className="flex-1 min-w-[200px]" />
|
||||
<button type="submit" className="btn btn-primary">Search</button>
|
||||
</form>
|
||||
<Link href="/admin/housekeeping?mode=manage" className="btn btn-outline">Manage</Link>
|
||||
<Link href="/admin/housekeeping?mode=import&form=selected_ids" className="btn btn-outline">Import selection</Link>
|
||||
</div>
|
||||
|
||||
{mode === "manage" ? (
|
||||
<section className="mt-6">
|
||||
<h2>Add / overwrite permission</h2>
|
||||
<form action={upsertPermission} className="card">
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<input name="permission" placeholder="permission key" required style={{ minWidth: 200 }} />
|
||||
<input name="minRank" type="number" min={0} placeholder="Min rank" required style={{ width: 110 }} />
|
||||
<input name="description" placeholder="description" style={{ flex: 1, minWidth: 160 }} />
|
||||
<section>
|
||||
<h2 className="admin-section-title">Add / overwrite permission</h2>
|
||||
<form action={upsertPermission} className="admin-card">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<input name="permission" placeholder="permission key" required className="min-w-[200px]" />
|
||||
<input name="minRank" type="number" min={0} placeholder="Min rank" required className="w-[110px]" />
|
||||
<input name="description" placeholder="description" className="flex-1 min-w-[160px]" />
|
||||
<button type="submit" className="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
) : (
|
||||
<section className="mt-6">
|
||||
<h2>Import Permissions</h2>
|
||||
<p className="muted" style={{ marginTop: "-0.4rem" }}>
|
||||
<section>
|
||||
<h2 className="admin-section-title">Import Permissions</h2>
|
||||
<p className="text-xs text-[var(--color-text-muted)] -mt-2 mb-3">
|
||||
Paste comma-separated permission IDs to import them as a selection.
|
||||
</p>
|
||||
<form action={upsertPermission} className="card">
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<input name="q" placeholder="Permission IDs (comma-separated)" required style={{ flex: 1, minWidth: 200 }} />
|
||||
<form action={upsertPermission} className="admin-card">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<input name="q" placeholder="Permission IDs (comma-separated)" required className="flex-1 min-w-[200px]" />
|
||||
<button type="submit" className="btn btn-primary">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="mt-6">
|
||||
<h2>
|
||||
<section>
|
||||
<h2 className="admin-section-title">
|
||||
Permissions <span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]">{filtered.length}</span>
|
||||
</h2>
|
||||
{filtered.length === 0 ? (
|
||||
<p className="muted">No housekeeping permissions match this search.</p>
|
||||
<div className="admin-empty">No housekeeping permissions match this search.</div>
|
||||
) : (
|
||||
<div className="card" style={{ padding: 0, overflowX: "auto" }}>
|
||||
<div className="overflow-x-auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: "40px" }}>#</th>
|
||||
<th className="w-[40px]">#</th>
|
||||
<th>Permission</th>
|
||||
<th>Description</th>
|
||||
<th style={{ width: 200 }}>Min rank</th>
|
||||
<th style={{ width: 80 }} />
|
||||
<th className="w-[200px]">Min rank</th>
|
||||
<th className="w-[80px]" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -16,36 +16,41 @@ export default async function AdminSettings() {
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>CMS Settings</h1>
|
||||
<p className="muted" style={{ marginTop: "-0.4rem" }}>
|
||||
Key/value config read at runtime via <code>siteSettings.get()</code>. Saving busts the cache.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
|
||||
<StatusCard label="Settings" value={settings.length} icon="🗄️" />
|
||||
<StatusCard label="With comment" value={documented} icon="📝" />
|
||||
<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">
|
||||
<span className="text-lg">⚙️</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="m-0 text-xl font-extrabold text-[var(--color-text)]">CMS Settings</h1>
|
||||
<p className="m-0 text-xs text-[var(--color-text-muted)] mt-0.5">
|
||||
Key/value config read at runtime via <code>siteSettings.get()</code>. Saving busts the cache.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2>Add / overwrite</h2>
|
||||
<form action={createSetting} className="card">
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
<div className="admin-stat-row">
|
||||
<span className="admin-stat-chip">{settings.length} settings</span>
|
||||
<span className="admin-stat-chip">{documented} with comment</span>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 className="admin-section-title">Add / overwrite</h2>
|
||||
<form action={createSetting} className="admin-card">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<input name="key" placeholder="key" required />
|
||||
<input name="value" placeholder="value" style={{ flex: 1, minWidth: 160 }} />
|
||||
<input name="value" placeholder="value" className="flex-1 min-w-[160px]" />
|
||||
<input name="comment" placeholder="comment" />
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2>
|
||||
<section>
|
||||
<h2 className="admin-section-title">
|
||||
Settings <span className="inline-block text-[0.72rem] font-bold px-2 py-0.5 rounded-full bg-[var(--color-primary)]/18 text-[var(--color-text)]">{settings.length}</span>
|
||||
</h2>
|
||||
{settings.length === 0 ? (
|
||||
<p className="muted">No settings yet.</p>
|
||||
<div className="admin-empty">No settings yet.</div>
|
||||
) : (
|
||||
<div className="grid">
|
||||
{settings.map((s) => (
|
||||
|
||||
+94
-12
@@ -79,6 +79,10 @@ html.dark textarea {
|
||||
background: #11151c;
|
||||
color: var(--color-text);
|
||||
}
|
||||
html.dark .admin-page table,
|
||||
html.dark .admin-card {
|
||||
border-color: rgba(255,255,255,0.06);
|
||||
}
|
||||
html.dark .currency,
|
||||
html.dark .article-img {
|
||||
background: #1f242d;
|
||||
@@ -436,46 +440,124 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
/* ── Admin panel (Housekeeping) ─────────────────────────────────
|
||||
Page content wrapper — card-style tables and consistent spacing. */
|
||||
Modern, consistent styling for all admin content pages. */
|
||||
.admin-page { scroll-margin-top: 1rem; }
|
||||
|
||||
/* Page heading */
|
||||
.admin-page main > h1:first-child,
|
||||
.admin-page > h1:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.admin-page h1 {
|
||||
font-size: 1.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--border);
|
||||
margin-bottom: 0.85rem;
|
||||
|
||||
/* Cards used inside admin pages */
|
||||
.admin-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-card);
|
||||
padding: 1.25rem;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
/* Card with filter/search bar — compact horizontal layout */
|
||||
.admin-filter-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.admin-filter-bar input,
|
||||
.admin-filter-bar select {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Compact stat row for list pages (counts, summaries) */
|
||||
.admin-stat-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.admin-stat-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
|
||||
border: 1px solid color-mix(in srgb, var(--color-primary) 15%, transparent);
|
||||
border-radius: 999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.admin-page table {
|
||||
width: 100%;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
.admin-page thead th {
|
||||
background: color-mix(in srgb, var(--color-primary) 10%, var(--color-surface));
|
||||
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
|
||||
color: var(--color-text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.7rem 0.9rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.75rem 0.9rem;
|
||||
}
|
||||
.admin-page td {
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-top: 1px solid color-mix(in srgb, var(--color-text-muted) 10%, transparent);
|
||||
border-top: 1px solid color-mix(in srgb, var(--color-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) 5%, transparent);
|
||||
background: color-mix(in srgb, var(--color-primary) 4%, transparent);
|
||||
}
|
||||
.admin-page thead tr:first-child th:first-child { border-radius: 16px 0 0 0; }
|
||||
.admin-page thead tr:first-child th:last-child { border-radius: 0 16px 0 0; }
|
||||
|
||||
/* Section heading inside admin pages */
|
||||
.admin-section-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.admin-section-title:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Two-column info grid */
|
||||
.admin-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.admin-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.admin-empty {
|
||||
text-align: center;
|
||||
padding: 2.5rem 1rem;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
/* ── Public design system (faithful to the atom theme) ──────────
|
||||
content-card = surface card with a primary-tinted header (icon +
|
||||
title + subtitle) and a padded body — the signature atom block. */
|
||||
|
||||
Reference in new issue
Block a user