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
+228 -41
View File
@@ -8,13 +8,22 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import * as LucideIcons from 'lucide-react';
const { ChevronDown, Lock } = LucideIcons;
const { ChevronDown, Lock, PanelLeftClose, PanelLeftOpen, FileCode } = LucideIcons;
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell';
import { SourceLink } from '@/components/common/SourceLink';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { IconTile } from '@/components/common/IconTile';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { sourceDownloadUrl } from '@/lib/sourceDownload';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { useUIStore } from '@/stores/uiStore';
import { useAccountStore } from '@/stores/accountStore';
import { useSchemaStore } from '@/stores/schemaStore';
@@ -150,11 +159,10 @@ function SidebarSubItem({
variant="ghost"
ref={isActive ? activeItemRef : undefined}
className={cn(
'w-full justify-start gap-2 font-normal',
isActive && 'bg-accent text-accent-foreground',
depth > 0 && 'text-sm',
'relative h-8 w-full justify-start gap-2 rounded-lg px-3 text-[13px] font-normal text-muted-foreground hover:bg-muted hover:text-foreground',
isActive && 'bg-accent font-medium text-accent-foreground hover:bg-accent hover:text-accent-foreground',
)}
style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
style={depth > 1 ? { paddingLeft: `${(depth - 1) * 12 + 12}px` } : undefined}
onClick={() => {
if (isLocked) {
onUpsell();
@@ -178,8 +186,8 @@ function SidebarSubItem({
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="w-full justify-start gap-2 font-normal text-sm"
style={{ paddingLeft: `${(depth + 1) * 12 + 8}px` }}
className="h-8 w-full justify-start gap-1.5 rounded-lg px-3 text-[13px] font-normal text-muted-foreground hover:bg-muted hover:text-foreground"
style={depth > 1 ? { paddingLeft: `${(depth - 1) * 12 + 12}px` } : undefined}
>
<ChevronDown className="h-3 w-3 shrink-0 transition-transform duration-200 [[data-state=closed]>&]:rotate-[-90deg]" />
<span className="truncate">{item.name}</span>
@@ -243,7 +251,10 @@ function SidebarTopItem({
<Button
variant="ghost"
ref={isActive ? activeItemRef : undefined}
className={cn('w-full justify-start gap-2 font-normal', isActive && 'bg-accent text-accent-foreground')}
className={cn(
'h-10 w-full justify-start gap-3 rounded-xl px-2 font-medium text-foreground/85 hover:bg-muted hover:text-foreground',
isActive && 'bg-accent text-accent-foreground hover:bg-accent',
)}
onClick={() => {
if (isLocked) {
onUpsell();
@@ -252,7 +263,7 @@ function SidebarTopItem({
}
}}
>
<LucideIcon name={icon} className="h-4 w-4 shrink-0" />
<IconTile name={icon} />
<span className="truncate">{name}</span>
{isLocked && <Lock className="ml-auto h-3 w-3 text-muted-foreground" />}
</Button>
@@ -268,13 +279,19 @@ function SidebarTopItem({
return (
<AutoOpenCollapsible containsActive={containsActive}>
<CollapsibleTrigger asChild>
<Button variant="ghost" className="w-full justify-start gap-2 font-normal">
<LucideIcon name={icon} className="h-4 w-4 shrink-0" />
<Button
variant="ghost"
className={cn(
'h-10 w-full justify-start gap-3 rounded-xl px-2 font-medium text-foreground/85 hover:bg-muted hover:text-foreground',
containsActive && 'text-foreground',
)}
>
<IconTile name={icon} />
<span className="truncate">{name}</span>
<ChevronDown className="ml-auto h-3 w-3 shrink-0 transition-transform duration-200 [[data-state=closed]>&]:rotate-[-90deg]" />
<ChevronDown className="ml-auto h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-200 [[data-state=closed]>&]:rotate-[-90deg]" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<CollapsibleContent className="ml-[1.35rem] mt-0.5 mb-1 space-y-0.5 border-l border-border pl-2.5">
{items.map((sub) => (
<SidebarSubItem
key={sub.type === 'link' ? sub.viewName : sub.name}
@@ -296,6 +313,93 @@ function SidebarTopItem({
return null;
}
/** INBUXA: every visible link under a container, flattened, for the rail's pop-out menu. */
function visibleLinks(items: LayoutSubItem[], edition: string, prefix = ''): { name: string; viewName: string }[] {
const out: { name: string; viewName: string }[] = [];
for (const it of items) {
if (it.type === 'link') {
if (!checkLinkVisible(it.viewName)) continue;
const enterprise = checkIsEnterprise(it.viewName);
if (enterprise && edition === 'oss') continue;
out.push({ name: `${prefix}${it.name || 'Overview'}`, viewName: it.viewName });
} else if (subtreeHasVisibleLink(it.items, edition)) {
out.push(...visibleLinks(it.items, edition, `${prefix}${it.name} `));
}
}
return out;
}
/** INBUXA: one entry of the collapsed sidebar: its tile, a label on hover, a menu for a group. */
function RailItem({
item,
sectionName,
currentPath,
navigate,
edition,
}: {
item: LayoutItem;
sectionName: string;
currentPath: string;
navigate: ReturnType<typeof useNavigate>;
edition: string;
}) {
const base = 'mx-auto flex h-11 w-11 items-center justify-center rounded-xl transition-colors hover:bg-muted';
if ('link' in item) {
const { name, icon, viewName } = item.link;
if (!checkLinkVisible(viewName)) return null;
const isActive = pathMatchesView(currentPath, sectionName, viewName);
return (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label={name}
aria-current={isActive ? 'page' : undefined}
className={cn(base, isActive && 'bg-accent')}
onClick={() => navigate(resolveViewPath(sectionName, viewName))}
>
<IconTile name={icon} />
</button>
</TooltipTrigger>
<TooltipContent side="right">{name}</TooltipContent>
</Tooltip>
);
}
const { name, icon, items } = item.container;
if (!subtreeHasVisibleLink(items, edition)) return null;
const links = visibleLinks(items, edition);
const containsActive = subtreeContainsActive(items, currentPath, sectionName);
return (
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<button type="button" aria-label={name} className={cn(base, containsActive && 'bg-accent')}>
<IconTile name={icon} />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="right">{name}</TooltipContent>
</Tooltip>
<DropdownMenuContent side="right" align="start" className="w-56">
<DropdownMenuLabel className="flex items-center gap-2">
<IconTile name={icon} size="sm" />
{name}
</DropdownMenuLabel>
{links.map((l) => (
<DropdownMenuItem
key={l.viewName}
className={cn(pathMatchesView(currentPath, sectionName, l.viewName) && 'bg-accent text-accent-foreground')}
onClick={() => navigate(resolveViewPath(sectionName, l.viewName))}
>
{l.name}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
export function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
@@ -303,6 +407,8 @@ export function Sidebar() {
const setActiveSection = useUIStore((s) => s.setActiveSection);
const sidebarOpen = useUIStore((s) => s.sidebarOpen);
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen);
const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed);
const toggleSidebarCollapsed = useUIStore((s) => s.toggleSidebarCollapsed);
const schema = useSchemaStore((s) => s.schema);
const edition = useAccountStore((s) => s.edition);
const permissions = useAccountStore((s) => s.permissions);
@@ -342,6 +448,83 @@ export function Sidebar() {
if (first) navigate(`/${target.name}/${first}`);
};
// Folding to a rail is for wide screens; a phone keeps the slide-over.
const collapsed = sidebarCollapsed && typeof window !== 'undefined' && window.matchMedia('(min-width: 768px)').matches;
if (collapsed) {
return (
<TooltipProvider delayDuration={150}>
<aside className="fixed top-14 left-0 bottom-0 z-30 flex w-[4.5rem] flex-col border-r bg-background">
<nav className="flex flex-1 flex-col gap-1 overflow-y-auto py-3 [scrollbar-width:none]">
{layout.items.map((item) => (
<RailItem
key={'link' in item ? item.link.viewName : item.container.name}
item={item}
sectionName={layout.name}
currentPath={location.pathname}
navigate={navigate}
edition={edition}
/>
))}
</nav>
<div className="flex flex-col items-center gap-1 border-t py-2">
{layouts.length > 1 &&
layouts.map((target) => {
const isActive = target.name === activeSection;
return (
<Tooltip key={target.name}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={target.name}
aria-current={isActive ? 'page' : undefined}
onClick={() => handleSectionClick(target)}
className={cn(
'flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground',
isActive && 'bg-card text-primary shadow-soft',
)}
>
<LucideIcon name={target.icon} className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">{target.name}</TooltipContent>
</Tooltip>
);
})}
<Tooltip>
<TooltipTrigger asChild>
<a
href={sourceDownloadUrl()}
download
aria-label="Source code of this version (AGPL-3.0)"
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground"
>
<FileCode className="h-4 w-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">
<SourceLink />
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="Expand sidebar"
onClick={toggleSidebarCollapsed}
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground"
>
<PanelLeftOpen className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">Expand sidebar</TooltipContent>
</Tooltip>
</div>
</aside>
</TooltipProvider>
);
}
return (
<>
<div
@@ -368,42 +551,46 @@ export function Sidebar() {
</div>
{layouts.length > 1 && (
<TooltipProvider>
<div className="flex items-center justify-around border-t bg-background px-2 py-2">
<div className="border-t px-3 pt-3 pb-2">
<div className="flex gap-1 rounded-xl bg-muted p-1" role="tablist">
{layouts.map((target) => {
const Icon = (LucideIcons as Record<string, unknown>)[
target.icon
.split('-')
.map((s) => s[0].toUpperCase() + s.slice(1))
.join('')
] as LucideIcons.LucideIcon | undefined;
const isActive = target.name === activeSection;
return (
<Tooltip key={target.name}>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
aria-label={target.name}
aria-current={isActive ? 'page' : undefined}
onClick={() => handleSectionClick(target)}
className={cn('h-9 w-9', isActive && 'bg-accent text-accent-foreground')}
>
{Icon ? <Icon className="h-4 w-4" /> : <LucideIcons.Circle className="h-4 w-4" />}
</Button>
</TooltipTrigger>
<TooltipContent side="top">{target.name}</TooltipContent>
</Tooltip>
<button
key={target.name}
type="button"
role="tab"
aria-selected={isActive}
aria-current={isActive ? 'page' : undefined}
onClick={() => handleSectionClick(target)}
className={cn(
'flex flex-1 flex-col items-center gap-0.5 rounded-lg px-1 py-1.5 text-[11px] font-medium text-muted-foreground transition-colors hover:text-foreground',
isActive && 'bg-card text-foreground shadow-soft',
)}
>
<LucideIcon name={target.icon} className={cn('h-4 w-4', isActive && 'text-primary')} />
<span className="truncate">{target.name}</span>
</button>
);
})}
</div>
</TooltipProvider>
</div>
)}
{/* INBUXA: the AGPL's offer, always on screen: the exact source of this version. */}
<div className="border-t px-3 py-2 text-center text-[11px] leading-tight text-muted-foreground">
<SourceLink className="underline-offset-2 hover:text-foreground hover:underline" />
<div className="flex items-center gap-2 border-t px-3 py-2">
<p className="min-w-0 flex-1 text-[11px] leading-tight text-muted-foreground">
<SourceLink className="underline-offset-2 hover:text-foreground hover:underline" />
</p>
<button
type="button"
aria-label="Collapse sidebar"
title="Collapse sidebar"
onClick={toggleSidebarCollapsed}
className="hidden h-8 w-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground md:flex"
>
<PanelLeftClose className="h-4 w-4" />
</button>
</div>
<EnterpriseUpsell open={upsellOpen} onClose={() => setUpsellOpen(false)} />