responsiveness and ui fixers

This commit is contained in:
2026-09-23 16:55:46 +05:30
parent 4c5e9b347a
commit f5e8e0e5b9
88 changed files with 9408 additions and 3576 deletions
@@ -1,5 +1,5 @@
"use client"
import React from "react"
import React, { useEffect, useState } from "react"
import {
Chart as ChartJS,
CategoryScale,
@@ -13,6 +13,24 @@ import { Bar } from "react-chartjs-2"
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
// Matches Tailwind's `sm` breakpoint — below it, axis labels get a smaller font
// so warehouse/category codes don't crowd or overlap on narrow chart widths.
const MOBILE_QUERY = "(max-width: 639px)"
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const mql = window.matchMedia(MOBILE_QUERY)
setIsMobile(mql.matches)
const onChange = (e: MediaQueryListEvent) => setIsMobile(e.matches)
mql.addEventListener("change", onChange)
return () => mql.removeEventListener("change", onChange)
}, [])
return isMobile
}
type Props = {
labels: string[]
datasets: Array<{
@@ -29,16 +47,32 @@ type Props = {
}
export default function BarChart({ labels, datasets, height = 260, barPercentage, categoryPercentage }: Props) {
const isMobile = useIsMobile()
const data = {
labels,
datasets: datasets.map((d) => ({ ...d, barPercentage, categoryPercentage })),
}
const tickFont = { size: isMobile ? 10 : 12 }
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: "top" as const } },
scales: { y: { beginAtZero: true } },
plugins: { legend: { position: "top" as const, labels: { font: tickFont } } },
scales: {
x: {
ticks: {
font: tickFont,
// Let Chart.js rotate labels to fit rather than silently dropping them via
// autoSkip — on a narrow phone width, angled text beats missing categories.
autoSkip: false,
maxRotation: 60,
minRotation: isMobile ? 45 : 0,
},
},
y: { beginAtZero: true, ticks: { font: tickFont } },
},
}
return (
@@ -17,7 +17,7 @@ export interface KpiTileSpec {
*/
export function KpiRow({ tiles }: { tiles: KpiTileSpec[] | null }) {
return (
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="mb-4 grid grid-cols-1 gap-3 sm:grid-cols-4">
{tiles
? tiles.map((tile) => (
<StatCard key={tile.label} label={tile.label} value={tile.value} icon={tile.icon} />
@@ -14,6 +14,9 @@ import { PaginationMeta } from "@/types/common"
interface PaginationFooterProps {
pagination: PaginationMeta
onPageChange: (page: number) => void
/** Rows-per-page choices — pass together with `onPageSizeChange` to show the selector. */
pageSizeOptions?: number[]
onPageSizeChange?: (size: number) => void
}
/** First, last, current ±1, and ellipses for the gaps — keeps the row from overflowing
@@ -30,9 +33,9 @@ function pageItems(current: number, total: number): (number | "ellipsis")[] {
/** Server-paginated table footer: "X – Y of Z" plus numbered page links, built on the
* shadcn-style pagination primitives. Use for any list page driven by a `PaginationMeta`
* from the API (as opposed to `DataTable`'s own client-side pagination). */
export function PaginationFooter({ pagination, onPageChange }: PaginationFooterProps) {
export function PaginationFooter({ pagination, onPageChange, pageSizeOptions, onPageSizeChange }: PaginationFooterProps) {
const { page, pageSize, totalItems, totalPages } = pagination
if (totalPages <= 1) return null
if (totalPages <= 1 && !pageSizeOptions) return null
const from = (page - 1) * pageSize + 1
const to = Math.min(page * pageSize, totalItems)
@@ -44,46 +47,63 @@ export function PaginationFooter({ pagination, onPageChange }: PaginationFooterP
return (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm text-muted-foreground">
{from} – {to} of {totalItems}
</p>
<Pagination className="mx-0 w-auto justify-end">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
text=""
onClick={(e) => go(e, page - 1)}
aria-disabled={page === 1}
className={page === 1 ? "pointer-events-none opacity-50" : undefined}
/>
</PaginationItem>
<div className="flex items-center gap-3">
<p className="text-sm text-muted-foreground">
{from} – {to} of {totalItems}
</p>
{pageSizeOptions && onPageSizeChange && (
<label className="flex items-center gap-1.5 text-sm text-muted-foreground">
Rows per page
<select
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
className="h-8 rounded-md border border-input bg-background px-2 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring/50"
aria-label="Rows per page"
>
{pageSizeOptions.map((size) => (
<option key={size} value={size}>{size}</option>
))}
</select>
</label>
)}
</div>
{totalPages > 1 && (
<Pagination className="mx-0 w-auto justify-end">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => go(e, page - 1)}
aria-disabled={page === 1}
className={page === 1 ? "pointer-events-none opacity-50" : undefined}
/>
</PaginationItem>
{pageItems(page, totalPages).map((item, i) =>
item === "ellipsis" ? (
<PaginationItem key={`ellipsis-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={item}>
<PaginationLink href="#" isActive={item === page} onClick={(e) => go(e, item)}>
{item}
</PaginationLink>
</PaginationItem>
)
)}
{pageItems(page, totalPages).map((item, i) =>
item === "ellipsis" ? (
<PaginationItem key={`ellipsis-${i}`}>
<PaginationEllipsis />
</PaginationItem>
) : (
<PaginationItem key={item}>
<PaginationLink href="#" isActive={item === page} onClick={(e) => go(e, item)}>
{item}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
href="#"
text=""
onClick={(e) => go(e, page + 1)}
aria-disabled={page === totalPages}
className={page === totalPages ? "pointer-events-none opacity-50" : undefined}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => go(e, page + 1)}
aria-disabled={page === totalPages}
className={page === totalPages ? "pointer-events-none opacity-50" : undefined}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
)
}
@@ -76,7 +76,6 @@ export function StatCard({
)}
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-muted-foreground">{label}</p>
<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">
+4 -4
View File
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("bg-muted/50 [&_tr]:border-b [&_tr]:hover:bg-muted/50", className)}
className={cn("bg-muted/50 [&_tr]:border-b [&_tr]:hover:bg-transparent", className)}
{...props}
/>
)
@@ -57,7 +57,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
"border-b transition-colors hover:bg-muted/20 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-semibold whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
"h-11 px-3 text-left align-middle text-sm font-semibold whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
@@ -83,7 +83,7 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
"px-3 py-4 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
)}
{...props}