ui and bug fixers 29/09

This commit is contained in:
2026-09-29 14:49:11 +05:30
parent d24072e20a
commit b2646c6cb3
85 changed files with 30050 additions and 750 deletions
+18 -2
View File
@@ -19,6 +19,12 @@ interface ComboboxProps<Value> {
emptyText?: string
disabled?: boolean
className?: string
/** Extra classes for the dropdown popup — e.g. a larger `min-w-*` so long labels fit
* when the trigger sits in a narrow table cell. */
popupClassName?: string
/** Always open below the input (never flip above it when space is short) — the list
* then scrolls within the space that is left below. */
openDownward?: boolean
"aria-invalid"?: boolean
}
@@ -32,6 +38,8 @@ export function Combobox<Value>({
emptyText = "No results found.",
disabled,
className,
popupClassName,
openDownward,
...rest
}: ComboboxProps<Value>) {
const itemToStringLabel = React.useCallback(
@@ -65,10 +73,18 @@ export function Combobox<Value>({
/>
</ComboboxPrimitive.InputGroup>
<ComboboxPrimitive.Portal>
<ComboboxPrimitive.Positioner side="bottom" sideOffset={4} className="isolate z-50">
<ComboboxPrimitive.Positioner
side="bottom"
sideOffset={4}
collisionAvoidance={openDownward ? { side: "none", align: "shift", fallbackAxisSide: "none" } : undefined}
className="isolate z-50"
>
<ComboboxPrimitive.Popup
data-slot="combobox-content"
className="max-h-(--available-height) w-(--anchor-width) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95"
className={cn(
"max-h-(--available-height) w-(--anchor-width) min-w-48 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
popupClassName
)}
>
<ComboboxPrimitive.Empty className="px-2.5 py-6 text-center text-sm text-muted-foreground">
{emptyText}
+14 -3
View File
@@ -7,6 +7,10 @@ export interface KpiTileSpec {
label: string
value: number
icon?: LucideIcon
/** Makes the tile clickable — e.g. to filter the list below it to this metric. */
onClick?: () => void
/** Highlights the tile as the currently-applied filter. Only meaningful with `onClick`. */
active?: boolean
}
/**
@@ -17,12 +21,19 @@ export interface KpiTileSpec {
*/
export function KpiRow({ tiles }: { tiles: KpiTileSpec[] | null }) {
return (
<div className="mb-4 grid grid-cols-1 gap-3 sm:grid-cols-4">
<div className="mb-4 flex flex-wrap gap-2">
{tiles
? tiles.map((tile) => (
<StatCard key={tile.label} label={tile.label} value={tile.value} icon={tile.icon} />
<StatCard
key={tile.label}
label={tile.label}
value={tile.value}
icon={tile.icon}
onClick={tile.onClick}
active={tile.active}
/>
))
: Array.from({ length: 2 }).map((_, i) => <Skeleton key={i} className="h-[90px] rounded-xl" />)}
: Array.from({ length: 2 }).map((_, i) => <Skeleton key={i} className="h-22.5 w-64 rounded-xl" />)}
</div>
)
}
+58 -57
View File
@@ -14,55 +14,39 @@ function formatValue(value: string | number) {
return formatted.replace(/(k|K|m|M|b|B)$/u, (s) => s.toUpperCase())
}
function Sparkline({ points }: { points: number[] }) {
const width = 48
const height = 18
const min = Math.min(...points)
const max = Math.max(...points)
const range = max - min || 1
const coords = points.map((point, index) => {
const x = (index / (points.length - 1)) * width
const y = height - ((point - min) / range) * height
return [x, y] as const
})
const path = coords.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x},${y}`).join(" ")
const [lastX, lastY] = coords[coords.length - 1]
return (
<svg
viewBox={`0 0 ${width} ${height}`}
className="h-4.5 w-12 shrink-0 overflow-visible"
aria-hidden="true"
>
<path d={path} fill="none" className="stroke-muted-foreground/40" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
<circle cx={lastX} cy={lastY} r={2.5} className="fill-primary stroke-card" strokeWidth={1.5} />
</svg>
)
}
export interface StatCardProps {
label: string
value: string | number
icon?: LucideIcon
/** Small subtitle under the value, e.g. "All time". */
caption?: string
delta?: number
deltaLabel?: string
/** Set true for metrics where an increase is bad (e.g. churn, costs). */
invert?: boolean
trend?: number[]
className?: string
/** Makes the card an actual button — e.g. to filter the list below it to this metric. */
onClick?: () => void
/** Highlights the card as the currently-applied filter. Only meaningful with `onClick`. */
active?: boolean
/** "sm" shrinks padding, icon and text — for tight rows of many cards (e.g. the Dashboard
* home overview) that need to fit without wrapping or scrolling. Defaults to "default". */
size?: "default" | "sm"
}
/** Icon in a circular badge on the left; label, value and an optional caption stacked to its right. */
export function StatCard({
label,
value,
icon: Icon,
caption,
delta,
deltaLabel,
invert = false,
trend,
className,
onClick,
active = false,
size = "default",
}: StatCardProps) {
const hasDelta = typeof delta === "number" && delta !== 0
const isPositive = hasDelta && delta > 0
@@ -70,39 +54,56 @@ export function StatCard({
return (
<div
role={onClick ? "button" : undefined}
tabIndex={onClick ? 0 : undefined}
onClick={onClick}
onKeyDown={
onClick
? (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
onClick()
}
}
: undefined
}
className={cn(
"flex flex-col gap-1.5 rounded-xl bg-card p-3.5 shadow-sm ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-primary/30",
"flex w-fit max-w-full shrink-0 items-center justify-self-start rounded-2xl bg-card shadow-sm ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg hover:ring-primary/30",
size === "sm" ? "min-w-52 gap-3.5 p-3.5" : "min-w-64 gap-3.5 p-4",
onClick && "cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-primary",
active && "ring-2 ring-primary hover:ring-primary",
className
)}
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-muted-foreground">{label}</p>
{Icon && (
<div className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-primary/10">
<Icon className="size-3.5 text-primary" />
</div>
)}
</div>
<div className="flex items-end justify-between gap-2">
<p className="text-lg font-bold tracking-tight text-foreground">{formatValue(value)}</p>
{trend && trend.length > 1 && <Sparkline points={trend} />}
</div>
{hasDelta && (
<div className="flex items-center gap-1 text-xs">
<span
className={cn(
"font-semibold",
isGood ? "text-success" : "text-destructive"
)}
>
{isPositive ? "+" : "-"}
{Math.abs(delta).toFixed(1)}%
</span>
{deltaLabel && <span className="text-muted-foreground">{deltaLabel}</span>}
{Icon && (
<div
className={cn(
"flex shrink-0 items-center justify-center rounded-full bg-primary/10",
size === "sm" ? "size-9" : "size-9"
)}
>
<Icon className={size === "sm" ? "size-4 text-primary" : "size-4 text-primary"} />
</div>
)}
<div className="flex min-w-0 flex-col gap-0.5">
<p className={cn("truncate font-medium text-muted-foreground", size === "sm" ? "text-sm" : "text-sm")}>{label}</p>
<p className={cn("font-bold tracking-tight text-foreground", size === "sm" ? "text-xl" : "text-xl")}>
{formatValue(value)}
</p>
{hasDelta ? (
<div className="flex items-center gap-1 text-xs">
<span className={cn("font-semibold", isGood ? "text-success" : "text-destructive")}>
{isPositive ? "+" : "-"}
{Math.abs(delta).toFixed(1)}%
</span>
{deltaLabel && <span className="text-muted-foreground">{deltaLabel}</span>}
</div>
) : (
caption && <p className="text-xs text-muted-foreground">{caption}</p>
)}
</div>
</div>
)
}