update ui design for all pages

This commit is contained in:
2026-08-12 18:56:56 +05:30
parent 25c4ee4a6a
commit 3beb66059b
58 changed files with 7892 additions and 1891 deletions

View File

@@ -7,6 +7,7 @@ import { SkeletonTable } from '@/components/ds/Skeleton';
import { LoadingOverlay } from '@/components/ds/LoadingState';
import { Pagination } from '@/components/ds/Pagination';
import { Checkbox } from '@/components/ui/checkbox';
import { usePreferences } from '@/lib/krowHooks';
/**
* DataTable — the app's one table.
@@ -60,6 +61,11 @@ export function DataTable({
stickyHeader = false,
}) {
/* ── Sorting ─────────────────────────────────────────────────────────── */
/* Row height follows the account's density preference. Only the vertical
padding changes — same columns, same type, same behaviour. */
const { compactDensity: compact } = usePreferences();
const cellPad = compact ? 'py-1.5' : 'py-3';
const [internalSort, setInternalSort] = React.useState(defaultSort ?? null);
const sort = controlledSort !== undefined ? controlledSort : internalSort;
@@ -193,7 +199,7 @@ export function DataTable({
<thead className={cn(stickyHeader && 'sticky top-0 z-10')}>
<tr className="bg-surface-subtle border-b border-border">
{selectable && (
<th scope="col" className="w-10 px-4 py-3">
<th scope="col" className={cn('w-10 px-4', cellPad)}>
<Checkbox
checked={allOnPageSelected ? true : someOnPageSelected ? 'indeterminate' : false}
onCheckedChange={toggleAll}
@@ -213,7 +219,8 @@ export function DataTable({
style={column.width ? { width: column.width } : undefined}
aria-sort={active ? (sort.direction === 'asc' ? 'ascending' : 'descending') : undefined}
className={cn(
'px-4 py-3 text-overline uppercase text-ink-4 font-heading whitespace-nowrap',
'px-4 text-overline uppercase text-ink-4 font-heading whitespace-nowrap',
cellPad,
column.align ? alignClass[column.align] : 'text-left',
column.hideBelow && hideBelowClass[column.hideBelow]
)}
@@ -256,7 +263,7 @@ export function DataTable({
)}
>
{selectable && (
<td className="px-4 py-3" onClick={(e) => e.stopPropagation()}>
<td className={cn('px-4', cellPad)} onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleRow(id)}
@@ -269,7 +276,8 @@ export function DataTable({
<td
key={column.key}
className={cn(
'px-4 py-3.5 text-body-sm text-ink-2 align-middle',
'px-4 text-body-sm text-ink-2 align-middle',
compact ? 'py-1.5' : 'py-3.5',
column.align ? alignClass[column.align] : 'text-left',
column.hideBelow && hideBelowClass[column.hideBelow],
column.className

View File

@@ -3,26 +3,33 @@ import { ArrowDownRight, ArrowUpRight, Minus } from 'lucide-react';
import { cn } from '@/lib/utils';
/**
* MetricStrip — a row of operational figures.
*
* The Admin surface reports six to eight numbers at once. As individual cards
* that becomes a wall of boxes competing with the content below it, so this
* presents them as one banded strip: hairline dividers instead of borders, one
* shared surface, numbers aligned on a baseline. Dense enough for an operations
* console, still readable.
* MetricStrip — modern executive operational metric card strip.
*
* `items: [{ label, value, delta?, deltaLabel?, invertTrend?, sub?, icon?, tone? }]`
*
* `invertTrend` exists because up is not always good: time-to-hire rising is
* bad, and colouring it green would misinform.
*/
const TONE = {
default: 'text-ink-1',
brand: 'text-krow-blue',
success: 'text-success',
warning: 'text-warning',
risk: 'text-destructive',
brand: 'text-blue-600 dark:text-blue-400',
success: 'text-emerald-600 dark:text-emerald-400',
warning: 'text-amber-600 dark:text-amber-400',
risk: 'text-red-600 dark:text-red-400',
};
const ICON_BG = {
default: 'bg-surface-subtle text-ink-3',
brand: 'bg-blue-50 text-blue-600 dark:bg-blue-950/60 dark:text-blue-400',
success: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-950/60 dark:text-emerald-400',
warning: 'bg-amber-50 text-amber-600 dark:bg-amber-950/60 dark:text-amber-400',
risk: 'bg-red-50 text-red-600 dark:bg-red-950/60 dark:text-red-400',
};
const ACCENT_BAR = {
default: 'from-gray-400 to-gray-600',
brand: 'from-blue-500 to-indigo-500',
success: 'from-emerald-500 to-teal-500',
warning: 'from-amber-500 to-orange-500',
risk: 'from-red-500 to-rose-500',
};
function Delta({ delta, label, invert }) {
@@ -35,79 +42,97 @@ function Delta({ delta, label, invert }) {
return (
<span
className={cn(
'inline-flex items-center gap-0.5 text-[11px] font-semibold tabular-nums',
flat ? 'text-ink-4' : good ? 'text-success' : 'text-destructive'
'inline-flex items-center gap-0.5 rounded-full px-2 py-0.5 text-[11px] font-semibold tabular-nums border',
flat
? 'bg-surface-subtle text-ink-4 border-border/60'
: good
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-400 border-emerald-200/50'
: 'bg-red-50 text-red-700 dark:bg-red-950/60 dark:text-red-400 border-red-200/50'
)}
>
<Icon className="h-3 w-3" aria-hidden="true" />
<Icon className="h-3 w-3 shrink-0" aria-hidden="true" />
{flat ? '0%' : `${Math.abs(delta)}%`}
{label && <span className="ml-1 font-normal text-ink-4">{label}</span>}
{label && <span className="ml-1 font-normal opacity-80">{label}</span>}
</span>
);
}
export function MetricStrip({ items = [], columns, className, loading = false }) {
// Column count follows the data by default, so the strip divides evenly
// instead of leaving an orphan cell.
const count = columns ?? Math.min(items.length, 6);
/**
* Five and six columns wait for `xl` rather than `lg`.
*
* The strip lives in the Admin main column, which is narrower than the
* viewport whenever the Owliver workspace is open — roughly 400px narrower. A
* `lg` breakpoint measures the window and so cheerfully packs six cells into a
* 580px column, where every label clips. Two rows of three is the better
* answer at that width.
*/
const gridCols = {
2: 'sm:grid-cols-2',
3: 'sm:grid-cols-3',
4: 'grid-cols-2 sm:grid-cols-4',
5: 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-5',
6: 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-6',
}[count] || 'sm:grid-cols-3';
}[count] || 'grid-cols-2 sm:grid-cols-4';
return (
<div
className={cn(
'grid overflow-hidden rounded-xl border border-border bg-surface',
// Hairline dividers rather than gaps: one object, not six.
'divide-x divide-y divide-border sm:divide-y-0',
gridCols,
className
)}
>
{items.map((item) => (
<div key={item.label} className="min-w-0 px-4 py-3.5">
<div className="flex items-center gap-1.5">
{item.icon && <item.icon className="h-3.5 w-3.5 shrink-0 text-ink-4" aria-hidden="true" />}
{/* Wraps rather than truncates. A clipped label is worse than a
two-line one, and grid rows stretch together so the strip stays
even either way. */}
<p className="text-[11px] font-medium uppercase leading-tight tracking-wide text-ink-4">
{item.label}
</p>
</div>
<div className={cn('grid gap-3.5', gridCols, className)}>
{items.map((item) => {
const IconComponent = item.icon;
const toneKey = item.tone || 'default';
{loading ? (
<div className="mt-1.5 h-7 w-16 animate-pulse rounded bg-surface-sunken" />
) : (
<div className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
<span
className={cn(
'font-heading text-title-lg font-bold tabular-nums leading-none',
TONE[item.tone] || TONE.default
return (
<div
key={item.label}
className="group relative flex flex-col justify-between rounded-xl border border-border/80 bg-surface p-4 shadow-xs backdrop-blur-xs transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md hover:border-blue-500/40"
>
<div>
{/* Header: Label & Icon */}
<div className="flex items-center justify-between gap-1.5">
<p className="truncate text-[10px] font-bold uppercase tracking-wider text-ink-4">
{item.label}
</p>
{IconComponent && (
<div
className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-lg shadow-xs',
ICON_BG[toneKey] || ICON_BG.default
)}
>
<IconComponent className="h-3.5 w-3.5" aria-hidden="true" />
</div>
)}
>
{item.value}
</span>
<Delta delta={item.delta} label={item.deltaLabel} invert={item.invertTrend} />
</div>
)}
</div>
{item.sub && <p className="mt-1 truncate text-caption text-ink-3">{item.sub}</p>}
</div>
))}
{/* Metric Main Display & Delta */}
{loading ? (
<div className="mt-2.5 h-8 w-16 animate-pulse rounded-lg bg-surface-sunken" />
) : (
<div className="mt-2 flex flex-wrap items-baseline gap-2">
<span
className={cn(
'font-heading text-title-xl font-bold leading-none tabular-nums',
TONE[toneKey] || TONE.default
)}
>
{item.value}
</span>
<Delta delta={item.delta} label={item.deltaLabel} invert={item.invertTrend} />
</div>
)}
</div>
{/* Sub-label & Accent bar */}
<div className="mt-3.5 space-y-2">
{item.sub && (
<span className="inline-flex items-center rounded-full bg-surface-subtle px-2 py-0.5 text-[10px] font-semibold text-ink-3 border border-border/60">
{item.sub}
</span>
)}
<div
className={cn(
'h-0.5 w-full rounded-full bg-gradient-to-r opacity-40 group-hover:opacity-100 transition-opacity',
ACCENT_BAR[toneKey] || ACCENT_BAR.default
)}
/>
</div>
</div>
);
})}
</div>
);
}