responsiveness and ui fixers
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user