A warmer, friendlier admin: first pass

- The INBUXA palette on warm surfaces, softer cards, buttons and inputs, and
  self-hosted Inter and Space Grotesk.
- Each section's icon sits on a colored tile, colored by what the section is
  about.
- The sidebar gets a guide line for sub-pages and a labeled Management /
  Settings / Account switch, and folds to a rail of tiles (remembered).
- Every page has a header with its section's tile.
- Fields and pages with no label of their own are spelled out in words
  (defaultCertificateId becomes Default certificate ID).
- The dashboard greets you, and its stat cards wear colored tiles.
- Unavailable live numbers are a calm note, not an error.
- The cat appears in empty lists and while loading.
- Chart colors work again: they were hex values wrapped in hsl().
This commit is contained in:
2026-09-19 00:38:53 -07:00
parent 92bcb58f76
commit e4ad5e2c1e
26 changed files with 789 additions and 161 deletions
+53
View File
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createElement } from 'react';
import * as LucideIcons from 'lucide-react';
import { cn } from '@/lib/utils';
import { toneFor, type Tone } from '@/lib/iconTones';
const TONE_CLASSES: Record<Tone, string> = {
teal: 'bg-teal-500/15 text-teal-700 dark:bg-teal-400/15 dark:text-teal-300',
orange: 'bg-orange-400/20 text-orange-700 dark:bg-orange-400/15 dark:text-orange-300',
sky: 'bg-sky-500/15 text-sky-700 dark:bg-sky-400/15 dark:text-sky-300',
violet: 'bg-violet-500/15 text-violet-700 dark:bg-violet-400/15 dark:text-violet-300',
rose: 'bg-rose-500/15 text-rose-700 dark:bg-rose-400/15 dark:text-rose-300',
amber: 'bg-amber-400/20 text-amber-700 dark:bg-amber-400/15 dark:text-amber-300',
emerald: 'bg-emerald-500/15 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300',
indigo: 'bg-indigo-500/15 text-indigo-700 dark:bg-indigo-400/15 dark:text-indigo-300',
slate: 'bg-slate-500/15 text-slate-700 dark:bg-slate-400/15 dark:text-slate-300',
};
function iconComponent(name: string): LucideIcons.LucideIcon {
const pascal = name
.split('-')
.map((s) => (s ? s[0].toUpperCase() + s.slice(1) : s))
.join('');
return ((LucideIcons as Record<string, unknown>)[pascal] as LucideIcons.LucideIcon | undefined) ?? LucideIcons.Circle;
}
/**
* A section's icon on a small colored tile. The color comes from what the
* section is about (see iconTones), so the same kind of thing looks the same
* everywhere, and a glance at the color finds it.
*/
export function IconTile({
name,
size = 'md',
className,
}: {
name: string;
size?: 'sm' | 'md' | 'lg';
className?: string;
}) {
const box = size === 'sm' ? 'h-6 w-6 rounded-md' : size === 'lg' ? 'h-10 w-10 rounded-xl' : 'h-7 w-7 rounded-lg';
const glyph = size === 'sm' ? 'h-3.5 w-3.5' : size === 'lg' ? 'h-5 w-5' : 'h-4 w-4';
return (
<span className={cn('inline-flex shrink-0 items-center justify-center', box, TONE_CLASSES[toneFor(name)], className)}>
{createElement(iconComponent(name), { className: glyph, strokeWidth: 2, 'aria-hidden': true })}
</span>
);
}