Merge pull request 'feat: enhance UI components and add vibrant theme support' (#17) from ui-fixers into Dev

Reviewed-on: #17
This commit was merged in pull request #17.
This commit is contained in:
2026-07-27 06:10:10 +00:00
56 changed files with 331 additions and 264 deletions
@@ -112,7 +112,7 @@ export default function AttendanceBatchDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">{batch.docNo}</h1> <h1 className="text-2xl font-bold text-foreground">{batch.docNo}</h1>
<p className="text-base text-muted-foreground">{new Date(batch.periodStart).toLocaleDateString()} {new Date(batch.periodEnd).toLocaleDateString()} · {batch.rowCountTotal} rows</p> <p className="text-base text-muted-foreground">{new Date(batch.periodStart).toLocaleDateString()} {new Date(batch.periodEnd).toLocaleDateString()} · {batch.rowCountTotal} rows</p>
@@ -132,9 +132,9 @@ export default function AttendanceBatchDetailPage() {
<FieldGroup> <FieldGroup>
<Field><FieldLabel htmlFor="u-reason">Reason</FieldLabel><Input id="u-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field> <Field><FieldLabel htmlFor="u-reason">Reason</FieldLabel><Input id="u-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
<Button className="min-w-32" onClick={unlock} disabled={busy}>Unlock</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -69,7 +69,7 @@ export default function AttendanceBatchesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Attendance</h1> <h1 className="text-2xl font-bold text-foreground">Attendance</h1>
<p className="text-base text-muted-foreground">Upload Preview Confirm. Once Confirmed, a batch becomes payroll&apos;s source of truth.</p> <p className="text-base text-muted-foreground">Upload Preview Confirm. Once Confirmed, a batch becomes payroll&apos;s source of truth.</p>
@@ -86,7 +86,7 @@ export default function AttendanceBatchesPage() {
<DialogDescription>Columns: Employee Code, Date, Check In, Check Out.</DialogDescription> <DialogDescription>Columns: Employee Code, Date, Check In, Check Out.</DialogDescription>
</DialogHeader> </DialogHeader>
<FieldGroup> <FieldGroup>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field><FieldLabel htmlFor="a-start">Period start</FieldLabel><Input id="a-start" type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} /></Field> <Field><FieldLabel htmlFor="a-start">Period start</FieldLabel><Input id="a-start" type="date" value={periodStart} onChange={(e) => setPeriodStart(e.target.value)} /></Field>
<Field><FieldLabel htmlFor="a-end">Period end</FieldLabel><Input id="a-end" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></Field> <Field><FieldLabel htmlFor="a-end">Period end</FieldLabel><Input id="a-end" type="date" value={periodEnd} onChange={(e) => setPeriodEnd(e.target.value)} /></Field>
</div> </div>
@@ -95,9 +95,9 @@ export default function AttendanceBatchesPage() {
<Input id="a-file" type="file" accept=".xlsx,.csv" onChange={(e) => setFile(e.target.files?.[0] ?? null)} /> <Input id="a-file" type="file" accept=".xlsx,.csv" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleUpload} disabled={submitting}><Upload className="size-4" />{submitting ? "Uploading…" : "Upload"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleUpload} disabled={submitting}><Upload className="size-4" />{submitting ? "Uploading…" : "Upload"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -208,7 +208,7 @@ export default function EmployeeDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">{employee.fullName}</h1> <h1 className="text-2xl font-bold text-foreground">{employee.fullName}</h1>
<p className="text-base text-muted-foreground"> <p className="text-base text-muted-foreground">
@@ -237,7 +237,7 @@ export default function EmployeeDetailPage() {
{tab === "Overview" && ( {tab === "Overview" && (
<FieldGroup className="max-w-2xl"> <FieldGroup className="max-w-2xl">
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field><FieldLabel>Full name</FieldLabel><Input value={employee.fullName} onChange={(e) => setEmployee({ ...employee, fullName: e.target.value })} /></Field> <Field><FieldLabel>Full name</FieldLabel><Input value={employee.fullName} onChange={(e) => setEmployee({ ...employee, fullName: e.target.value })} /></Field>
<Field><FieldLabel>Email</FieldLabel><Input type="email" value={employee.email ?? ""} onChange={(e) => setEmployee({ ...employee, email: e.target.value })} /></Field> <Field><FieldLabel>Email</FieldLabel><Input type="email" value={employee.email ?? ""} onChange={(e) => setEmployee({ ...employee, email: e.target.value })} /></Field>
<Field><FieldLabel>NIC</FieldLabel><Input value={employee.nic ?? ""} onChange={(e) => setEmployee({ ...employee, nic: e.target.value })} /></Field> <Field><FieldLabel>NIC</FieldLabel><Input value={employee.nic ?? ""} onChange={(e) => setEmployee({ ...employee, nic: e.target.value })} /></Field>
@@ -363,7 +363,7 @@ export default function EmployeeDetailPage() {
<DialogDescription>Supersedes the current open-ended structure from this date.</DialogDescription> <DialogDescription>Supersedes the current open-ended structure from this date.</DialogDescription>
</DialogHeader> </DialogHeader>
<FieldGroup> <FieldGroup>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field><FieldLabel>Effective from</FieldLabel><Input type="date" value={structureEffectiveFrom} onChange={(e) => setStructureEffectiveFrom(e.target.value)} /></Field> <Field><FieldLabel>Effective from</FieldLabel><Input type="date" value={structureEffectiveFrom} onChange={(e) => setStructureEffectiveFrom(e.target.value)} /></Field>
<Field><FieldLabel>Basic salary</FieldLabel><Input type="number" min={0} value={structureBasic} onChange={(e) => setStructureBasic(Number(e.target.value))} /></Field> <Field><FieldLabel>Basic salary</FieldLabel><Input type="number" min={0} value={structureBasic} onChange={(e) => setStructureBasic(Number(e.target.value))} /></Field>
</div> </div>
@@ -383,9 +383,9 @@ export default function EmployeeDetailPage() {
</div> </div>
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setStructureOpen(false)} disabled={busy}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setStructureOpen(false)} disabled={busy}>Cancel</Button>
<Button className="min-w-32" onClick={createStructure} disabled={busy}>{busy ? "Saving…" : "Save"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={createStructure} disabled={busy}>{busy ? "Saving…" : "Save"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -433,7 +433,7 @@ export default function EmployeeDetailPage() {
<SelectContent><SelectItem value="Loan">Loan</SelectItem><SelectItem value="Advance">Advance</SelectItem></SelectContent> <SelectContent><SelectItem value="Loan">Loan</SelectItem><SelectItem value="Advance">Advance</SelectItem></SelectContent>
</Select> </Select>
</Field> </Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field><FieldLabel>Principal</FieldLabel><Input type="number" min={0} value={loanPrincipal} onChange={(e) => setLoanPrincipal(Number(e.target.value))} /></Field> <Field><FieldLabel>Principal</FieldLabel><Input type="number" min={0} value={loanPrincipal} onChange={(e) => setLoanPrincipal(Number(e.target.value))} /></Field>
<Field><FieldLabel>Installment amount</FieldLabel><Input type="number" min={0} value={loanInstallmentAmount} onChange={(e) => setLoanInstallmentAmount(Number(e.target.value))} /></Field> <Field><FieldLabel>Installment amount</FieldLabel><Input type="number" min={0} value={loanInstallmentAmount} onChange={(e) => setLoanInstallmentAmount(Number(e.target.value))} /></Field>
<Field><FieldLabel># installments</FieldLabel><Input type="number" min={1} value={loanCount} onChange={(e) => setLoanCount(Number(e.target.value))} /></Field> <Field><FieldLabel># installments</FieldLabel><Input type="number" min={1} value={loanCount} onChange={(e) => setLoanCount(Number(e.target.value))} /></Field>
@@ -445,9 +445,9 @@ export default function EmployeeDetailPage() {
</Field> </Field>
</div> </div>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setLoanOpen(false)} disabled={busy}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setLoanOpen(false)} disabled={busy}>Cancel</Button>
<Button className="min-w-32" onClick={createLoan} disabled={busy}>{busy ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={createLoan} disabled={busy}>{busy ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -121,7 +121,7 @@ export default function EmployeesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Employees</h1> <h1 className="text-2xl font-bold text-foreground">Employees</h1>
<p className="text-base text-muted-foreground">Staff records separate from system login accounts (see the link chip below).</p> <p className="text-base text-muted-foreground">Staff records separate from system login accounts (see the link chip below).</p>
@@ -134,7 +134,7 @@ export default function EmployeesPage() {
<DialogDescription>Not every employee needs a login a system user account is optional and separate.</DialogDescription> <DialogDescription>Not every employee needs a login a system user account is optional and separate.</DialogDescription>
</DialogHeader> </DialogHeader>
<FieldGroup> <FieldGroup>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field data-invalid={!!errors.employeeCode}> <Field data-invalid={!!errors.employeeCode}>
<FieldLabel htmlFor="e-code">Employee code</FieldLabel> <FieldLabel htmlFor="e-code">Employee code</FieldLabel>
<Input id="e-code" value={form.employeeCode} onChange={(e) => setForm((f) => ({ ...f, employeeCode: e.target.value }))} aria-invalid={!!errors.employeeCode} /> <Input id="e-code" value={form.employeeCode} onChange={(e) => setForm((f) => ({ ...f, employeeCode: e.target.value }))} aria-invalid={!!errors.employeeCode} />
@@ -170,7 +170,7 @@ export default function EmployeesPage() {
</div> </div>
)} )}
</Field> </Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field data-invalid={!!errors.departmentId}> <Field data-invalid={!!errors.departmentId}>
<FieldLabel htmlFor="e-dept">Department</FieldLabel> <FieldLabel htmlFor="e-dept">Department</FieldLabel>
<Select value={form.departmentId ? String(form.departmentId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, departmentId: Number(v) }))}> <Select value={form.departmentId ? String(form.departmentId) : undefined} onValueChange={(v) => setForm((f) => ({ ...f, departmentId: Number(v) }))}>
@@ -205,9 +205,9 @@ export default function EmployeesPage() {
</Field> </Field>
</div> </div>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -102,7 +102,7 @@ export default function LeaveRequestsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Leave</h1> <h1 className="text-2xl font-bold text-foreground">Leave</h1>
<p className="text-base text-muted-foreground">Approved leave feeds Attendance&apos;s OnLeave status and Payroll&apos;s No-Pay calculation.</p> <p className="text-base text-muted-foreground">Approved leave feeds Attendance&apos;s OnLeave status and Payroll&apos;s No-Pay calculation.</p>
@@ -129,15 +129,15 @@ export default function LeaveRequestsPage() {
<SelectContent>{leaveTypes.map((t) => <SelectItem key={t.leaveTypeId} value={String(t.leaveTypeId)}>{t.name}</SelectItem>)}</SelectContent> <SelectContent>{leaveTypes.map((t) => <SelectItem key={t.leaveTypeId} value={String(t.leaveTypeId)}>{t.name}</SelectItem>)}</SelectContent>
</Select> </Select>
</Field> </Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field><FieldLabel>Start date</FieldLabel><Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} /></Field> <Field><FieldLabel>Start date</FieldLabel><Input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} /></Field>
<Field><FieldLabel>End date</FieldLabel><Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /></Field> <Field><FieldLabel>End date</FieldLabel><Input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /></Field>
</div> </div>
<Field><FieldLabel>Reason (optional)</FieldLabel><Input value={reason} onChange={(e) => setReason(e.target.value)} /></Field> <Field><FieldLabel>Reason (optional)</FieldLabel><Input value={reason} onChange={(e) => setReason(e.target.value)} /></Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Submitting…" : "Submit"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Submitting…" : "Submit"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -64,7 +64,7 @@ export default function PayrollLineDetailPage() {
</table> </table>
</div> </div>
<div className="grid grid-cols-3 gap-3 text-sm text-muted-foreground"> <div className="grid grid-cols-2 gap-3 text-sm text-muted-foreground sm:grid-cols-3">
<div>Present days: {line.presentDays}</div> <div>Present days: {line.presentDays}</div>
<div>Absent days: {line.absentDays}</div> <div>Absent days: {line.absentDays}</div>
<div>Leave days: {line.leaveDays}</div> <div>Leave days: {line.leaveDays}</div>
@@ -88,12 +88,12 @@ export default function PayrollRunDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">{run.docNo}</h1> <h1 className="text-2xl font-bold text-foreground">{run.docNo}</h1>
<p className="text-base text-muted-foreground">{run.periodMonth.toString().padStart(2, "0")}/{run.periodYear} · {run.employeeCount} employees</p> <p className="text-base text-muted-foreground">{run.periodMonth.toString().padStart(2, "0")}/{run.periodYear} · {run.employeeCount} employees</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<Badge variant="outline" className="h-7 px-3 text-sm">{run.status}</Badge> <Badge variant="outline" className="h-7 px-3 text-sm">{run.status}</Badge>
{run.status === "Draft" && <Button onClick={approve} disabled={busy}>Approve</Button>} {run.status === "Draft" && <Button onClick={approve} disabled={busy}>Approve</Button>}
{run.status === "Approved" && <Button onClick={lock} disabled={busy}>Lock Payroll</Button>} {run.status === "Approved" && <Button onClick={lock} disabled={busy}>Lock Payroll</Button>}
@@ -110,9 +110,9 @@ export default function PayrollRunDetailPage() {
<FieldGroup> <FieldGroup>
<Field><FieldLabel htmlFor="p-reason">Reason</FieldLabel><Input id="p-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field> <Field><FieldLabel htmlFor="p-reason">Reason</FieldLabel><Input id="p-reason" value={unlockReason} onChange={(e) => setUnlockReason(e.target.value)} /></Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setUnlockOpen(false)}>Cancel</Button>
<Button className="min-w-32" onClick={unlock} disabled={busy}>Unlock</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={unlock} disabled={busy}>Unlock</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -63,7 +63,7 @@ export default function PayrollRunsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Payroll</h1> <h1 className="text-2xl font-bold text-foreground">Payroll</h1>
<p className="text-base text-muted-foreground">Generate Review Approve Lock Payslips.</p> <p className="text-base text-muted-foreground">Generate Review Approve Lock Payslips.</p>
@@ -76,14 +76,14 @@ export default function PayrollRunsPage() {
<DialogDescription>Blocked if attendance for this period isn&apos;t fully Confirmed yet.</DialogDescription> <DialogDescription>Blocked if attendance for this period isn&apos;t fully Confirmed yet.</DialogDescription>
</DialogHeader> </DialogHeader>
<FieldGroup> <FieldGroup>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field><FieldLabel>Year</FieldLabel><Input type="number" value={periodYear} onChange={(e) => setPeriodYear(Number(e.target.value))} /></Field> <Field><FieldLabel>Year</FieldLabel><Input type="number" value={periodYear} onChange={(e) => setPeriodYear(Number(e.target.value))} /></Field>
<Field><FieldLabel>Month</FieldLabel><Input type="number" min={1} max={12} value={periodMonth} onChange={(e) => setPeriodMonth(Number(e.target.value))} /></Field> <Field><FieldLabel>Month</FieldLabel><Input type="number" min={1} max={12} value={periodMonth} onChange={(e) => setPeriodMonth(Number(e.target.value))} /></Field>
</div> </div>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleGenerate} disabled={submitting}>{submitting ? "Generating…" : "Generate"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleGenerate} disabled={submitting}>{submitting ? "Generating…" : "Generate"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -92,7 +92,7 @@ export default function DepartmentsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Departments</h1> <h1 className="text-2xl font-bold text-foreground">Departments</h1>
<p className="text-base text-muted-foreground">Org structure unlimited nesting, optionally scoped to a branch.</p> <p className="text-base text-muted-foreground">Org structure unlimited nesting, optionally scoped to a branch.</p>
@@ -140,9 +140,9 @@ export default function DepartmentsPage() {
</Select> </Select>
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -79,7 +79,7 @@ export default function DocumentTypesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Document Types</h1> <h1 className="text-2xl font-bold text-foreground">Document Types</h1>
<p className="text-base text-muted-foreground">The staff document catalog NIC, contracts, certificates, etc.</p> <p className="text-base text-muted-foreground">The staff document catalog NIC, contracts, certificates, etc.</p>
@@ -120,9 +120,9 @@ export default function DocumentTypesPage() {
<FieldLabel htmlFor="dt-expiry">Track expiry date (e.g. passport, visa)</FieldLabel> <FieldLabel htmlFor="dt-expiry">Track expiry date (e.g. passport, visa)</FieldLabel>
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -81,7 +81,7 @@ export default function LeaveTypesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Leave Types</h1> <h1 className="text-2xl font-bold text-foreground">Leave Types</h1>
<p className="text-base text-muted-foreground">Annual, Casual, Medical, Unpaid, etc. drives Attendance&apos;s OnLeave classification and Payroll&apos;s No-Pay calc.</p> <p className="text-base text-muted-foreground">Annual, Casual, Medical, Unpaid, etc. drives Attendance&apos;s OnLeave classification and Payroll&apos;s No-Pay calc.</p>
@@ -121,9 +121,9 @@ export default function LeaveTypesPage() {
<FieldLabel htmlFor="lt-carry">Carry-forward allowed</FieldLabel> <FieldLabel htmlFor="lt-carry">Carry-forward allowed</FieldLabel>
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -78,7 +78,7 @@ export default function SalaryComponentsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Salary Components</h1> <h1 className="text-2xl font-bold text-foreground">Salary Components</h1>
<p className="text-base text-muted-foreground">Allowances and ad hoc other deductions OT/Late/No-Pay/Loan/EPF/ETF/Tax are computed automatically, not components.</p> <p className="text-base text-muted-foreground">Allowances and ad hoc other deductions OT/Late/No-Pay/Loan/EPF/ETF/Tax are computed automatically, not components.</p>
@@ -120,9 +120,9 @@ export default function SalaryComponentsPage() {
<FieldLabel htmlFor="sc-epfetf">EPF/ETF applicable</FieldLabel> <FieldLabel htmlFor="sc-epfetf">EPF/ETF applicable</FieldLabel>
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -85,7 +85,7 @@ export default function StatutorySettingsPage() {
{error && <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>} {error && <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>}
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold text-foreground">EPF / ETF Rates</h2> <h2 className="text-lg font-semibold text-foreground">EPF / ETF Rates</h2>
<Dialog open={settingOpen} onOpenChange={setSettingOpen}> <Dialog open={settingOpen} onOpenChange={setSettingOpen}>
<DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Setting</Button>} /> <DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Setting</Button>} />
@@ -101,9 +101,9 @@ export default function StatutorySettingsPage() {
<Field><FieldLabel>ETF employer rate (0-1)</FieldLabel><Input type="number" step="0.001" value={etfEmployerRate} onChange={(e) => setEtfEmployerRate(Number(e.target.value))} /></Field> <Field><FieldLabel>ETF employer rate (0-1)</FieldLabel><Input type="number" step="0.001" value={etfEmployerRate} onChange={(e) => setEtfEmployerRate(Number(e.target.value))} /></Field>
<Field><FieldLabel>Default OT multiplier</FieldLabel><Input type="number" step="0.1" value={otMultiplierDefault} onChange={(e) => setOtMultiplierDefault(Number(e.target.value))} /></Field> <Field><FieldLabel>Default OT multiplier</FieldLabel><Input type="number" step="0.1" value={otMultiplierDefault} onChange={(e) => setOtMultiplierDefault(Number(e.target.value))} /></Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setSettingOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setSettingOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={createSetting} disabled={submitting}>{submitting ? "Saving…" : "Save"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={createSetting} disabled={submitting}>{submitting ? "Saving…" : "Save"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -133,7 +133,7 @@ export default function StatutorySettingsPage() {
</section> </section>
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 className="text-lg font-semibold text-foreground">Tax Slabs</h2> <h2 className="text-lg font-semibold text-foreground">Tax Slabs</h2>
<Dialog open={slabOpen} onOpenChange={setSlabOpen}> <Dialog open={slabOpen} onOpenChange={setSlabOpen}>
<DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Slab</Button>} /> <DialogTrigger render={<Button size="sm"><Plus className="size-4" />New Slab</Button>} />
@@ -148,9 +148,9 @@ export default function StatutorySettingsPage() {
<Field><FieldLabel>Upper bound (optional)</FieldLabel><Input type="number" value={upperBound} onChange={(e) => setUpperBound(e.target.value)} placeholder="And above" /></Field> <Field><FieldLabel>Upper bound (optional)</FieldLabel><Input type="number" value={upperBound} onChange={(e) => setUpperBound(e.target.value)} placeholder="And above" /></Field>
<Field><FieldLabel>Rate (0-1)</FieldLabel><Input type="number" step="0.01" value={rate} onChange={(e) => setRate(Number(e.target.value))} /></Field> <Field><FieldLabel>Rate (0-1)</FieldLabel><Input type="number" step="0.01" value={rate} onChange={(e) => setRate(Number(e.target.value))} /></Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setSlabOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setSlabOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={createSlab} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={createSlab} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -99,7 +99,7 @@ export default function WorkShiftsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Work Shifts</h1> <h1 className="text-2xl font-bold text-foreground">Work Shifts</h1>
<p className="text-base text-muted-foreground">The baseline Attendance computes Late/Early/OT against.</p> <p className="text-base text-muted-foreground">The baseline Attendance computes Late/Early/OT against.</p>
@@ -122,7 +122,7 @@ export default function WorkShiftsPage() {
<Input id="w-name" value={name} onChange={(e) => setName(e.target.value)} aria-invalid={!!errors.name} /> <Input id="w-name" value={name} onChange={(e) => setName(e.target.value)} aria-invalid={!!errors.name} />
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} /> <FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field> </Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field> <Field>
<FieldLabel htmlFor="w-start">Start time</FieldLabel> <FieldLabel htmlFor="w-start">Start time</FieldLabel>
<Input id="w-start" type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} /> <Input id="w-start" type="time" value={startTime} onChange={(e) => setStartTime(e.target.value)} />
@@ -136,7 +136,7 @@ export default function WorkShiftsPage() {
<Checkbox id="w-overnight" checked={isOvernight} onCheckedChange={(v) => setIsOvernight(v === true)} /> <Checkbox id="w-overnight" checked={isOvernight} onCheckedChange={(v) => setIsOvernight(v === true)} />
<FieldLabel htmlFor="w-overnight">Overnight shift (end time rolls past midnight)</FieldLabel> <FieldLabel htmlFor="w-overnight">Overnight shift (end time rolls past midnight)</FieldLabel>
</Field> </Field>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field> <Field>
<FieldLabel htmlFor="w-grace">Grace (minutes)</FieldLabel> <FieldLabel htmlFor="w-grace">Grace (minutes)</FieldLabel>
<Input id="w-grace" type="number" min={0} value={graceMinutes} onChange={(e) => setGraceMinutes(Number(e.target.value))} /> <Input id="w-grace" type="number" min={0} value={graceMinutes} onChange={(e) => setGraceMinutes(Number(e.target.value))} />
@@ -166,9 +166,9 @@ export default function WorkShiftsPage() {
</div> </div>
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button> <Button variant="outline" className="w-full sm:w-auto sm:min-w-32" onClick={() => setOpen(false)} disabled={submitting}>Cancel</Button>
<Button className="min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button> <Button className="w-full sm:w-auto sm:min-w-32" onClick={handleCreate} disabled={submitting}>{submitting ? "Creating…" : "Create"}</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+3 -3
View File
@@ -13,10 +13,10 @@ export default function DashboardLayout({
<AuthProvider> <AuthProvider>
<div className="flex h-screen overflow-hidden bg-background"> <div className="flex h-screen overflow-hidden bg-background">
<AppSidebar /> <AppSidebar />
<main className="flex flex-1 flex-col"> <main className="flex min-w-0 flex-1 flex-col">
<Header /> <Header />
<div className="flex-1 overflow-auto"> <div className="min-w-0 flex-1 overflow-auto">
<div className="p-6 lg:p-8"> <div className="min-w-0 px-6 pt-3 pb-6 lg:px-8 lg:pt-4 lg:pb-8">
<Breadcrumbs /> <Breadcrumbs />
<div className="rounded-xl bg-card border border-border shadow-sm"> <div className="rounded-xl bg-card border border-border shadow-sm">
<div className="p-6"> <div className="p-6">
@@ -267,7 +267,7 @@ export default function PurchaseOrderDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/procurement/purchase-orders" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/procurement/purchase-orders" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -283,7 +283,7 @@ export default function PurchaseOrderDetailPage() {
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{po.status === "Draft" && ( {po.status === "Draft" && (
<> <>
<Button variant="outline" size="lg" onClick={handleSubmitPo} disabled={submitting || deleting}> <Button variant="outline" size="lg" onClick={handleSubmitPo} disabled={submitting || deleting}>
@@ -64,7 +64,7 @@ export default function PurchaseOrdersListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Purchase Orders</h1> <h1 className="text-2xl font-bold text-foreground">Purchase Orders</h1>
<p className="text-base text-muted-foreground">Auto-approved on creation and freely editable while open (FR-PROC-03..05).</p> <p className="text-base text-muted-foreground">Auto-approved on creation and freely editable while open (FR-PROC-03..05).</p>
@@ -48,7 +48,7 @@ export default function PurchaseReturnsListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Purchase Returns</h1> <h1 className="text-2xl font-bold text-foreground">Purchase Returns</h1>
<p className="text-base text-muted-foreground">Return received goods to a vendor, referencing the original GRN line (FR-PROC-08).</p> <p className="text-base text-muted-foreground">Return received goods to a vendor, referencing the original GRN line (FR-PROC-08).</p>
@@ -72,7 +72,7 @@ export default function RequisitionDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/procurement/requisitions" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/procurement/requisitions" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -86,7 +86,7 @@ export default function RequisitionDetailPage() {
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{requisition.status === "Draft" && ( {requisition.status === "Draft" && (
<Button size="lg" onClick={handleSubmit} disabled={submitting}> <Button size="lg" onClick={handleSubmit} disabled={submitting}>
<Send className="size-5" /> <Send className="size-5" />
@@ -43,7 +43,7 @@ export default function RequisitionsListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Requisitions</h1> <h1 className="text-2xl font-bold text-foreground">Requisitions</h1>
<p className="text-base text-muted-foreground">Raise a purchase requisition and submit it into procurement (FR-PROC-01).</p> <p className="text-base text-muted-foreground">Raise a purchase requisition and submit it into procurement (FR-PROC-01).</p>
@@ -28,7 +28,7 @@ export default function RfqsListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">RFQs</h1> <h1 className="text-2xl font-bold text-foreground">RFQs</h1>
<p className="text-base text-muted-foreground">Request quotations from vendors and compare pricing (FR-PROC-02).</p> <p className="text-base text-muted-foreground">Request quotations from vendors and compare pricing (FR-PROC-02).</p>
@@ -181,7 +181,7 @@ export default function ItemDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -155,7 +155,7 @@ export default function BrandsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -180,11 +180,11 @@ export default function BrandsPage() {
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} /> <FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"} {submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
</Button> </Button>
</div> </div>
@@ -111,7 +111,7 @@ export default function CategorySubCategoriesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/products/categories" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/products/categories" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -138,11 +138,11 @@ export default function CategorySubCategoriesPage() {
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} /> <FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"} {submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
</Button> </Button>
</div> </div>
@@ -154,7 +154,7 @@ export default function CategoriesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -179,11 +179,11 @@ export default function CategoriesPage() {
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} /> <FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"} {submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
</Button> </Button>
</div> </div>
@@ -108,7 +108,7 @@ export default function ItemTypesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -141,11 +141,11 @@ export default function ItemTypesPage() {
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} /> <FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleSubmit} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleSubmit} disabled={submitting}>
{submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"} {submitting ? (editing ? "Saving…" : "Creating…") : editing ? "Save" : "Create"}
</Button> </Button>
</div> </div>
@@ -100,7 +100,7 @@ export default function ItemsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Items</h1> <h1 className="text-2xl font-bold text-foreground">Items</h1>
<p className="text-base text-muted-foreground">Item master SKU, tracking mode, category, default vendor (FR-MD-01).</p> <p className="text-base text-muted-foreground">Item master SKU, tracking mode, category, default vendor (FR-MD-01).</p>
@@ -57,7 +57,7 @@ export default function UomsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -82,11 +82,11 @@ export default function UomsPage() {
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} /> <FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
{submitting ? "Creating…" : "Create"} {submitting ? "Creating…" : "Create"}
</Button> </Button>
</div> </div>
@@ -116,7 +116,7 @@ export default function GrnDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/receiving/grn" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/receiving/grn" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -400,20 +400,7 @@ export default function NewGrnPage() {
)} )}
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-base font-semibold text-foreground">Lines</h2>
{mode === "po" && (
<p className="text-sm text-muted-foreground">
Lines default from the PO&apos;s open quantities add a row for anything received that wasn&apos;t ordered.
</p>
)}
</div>
<Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}>
<Plus className="size-5" />
Add line
</Button>
<div className="flex items-center justify-between gap-3">
<div> <div>
<h2 className="text-base font-semibold text-foreground">Lines</h2> <h2 className="text-base font-semibold text-foreground">Lines</h2>
{mode === "po" && ( {mode === "po" && (
@@ -422,7 +409,7 @@ export default function NewGrnPage() {
</p> </p>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
{/* Off-PO items are allowed on a PO-based GRN — the server treats a line with {/* Off-PO items are allowed on a PO-based GRN — the server treats a line with
no poLineId as a direct receipt (docs/10 FR-GRN-01, revised). */} no poLineId as a direct receipt (docs/10 FR-GRN-01, revised). */}
<Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}> <Button type="button" variant="outline" onClick={() => setLines((prev) => [...prev, emptyLine()])}>
@@ -72,7 +72,7 @@ export default function GrnListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Goods Receipt Notes</h1> <h1 className="text-2xl font-bold text-foreground">Goods Receipt Notes</h1>
<p className="text-base text-muted-foreground"> <p className="text-base text-muted-foreground">
@@ -123,7 +123,7 @@ export default function RolesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Roles</h1> <h1 className="text-2xl font-bold text-foreground">Roles</h1>
<p className="text-base text-muted-foreground"> <p className="text-base text-muted-foreground">
@@ -182,11 +182,11 @@ export default function RolesPage() {
)} )}
</div> </div>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
{submitting ? "Creating…" : "Create"} {submitting ? "Creating…" : "Create"}
</Button> </Button>
</div> </div>
@@ -147,7 +147,7 @@ export default function UsersPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Users</h1> <h1 className="text-2xl font-bold text-foreground">Users</h1>
<p className="text-base text-muted-foreground"> <p className="text-base text-muted-foreground">
@@ -242,11 +242,11 @@ export default function UsersPage() {
<FieldError errors={[errors.userTypeId ? { message: errors.userTypeId } : undefined]} /> <FieldError errors={[errors.userTypeId ? { message: errors.userTypeId } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
{submitting ? "Creating…" : "Create"} {submitting ? "Creating…" : "Create"}
</Button> </Button>
</div> </div>
@@ -37,7 +37,7 @@ export default function AdjustmentsListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -89,7 +89,7 @@ export default function CountDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/stock/counts" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/stock/counts" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -34,7 +34,7 @@ export default function CountsListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -83,7 +83,7 @@ export default function TransferDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/stock/transfers" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/stock/transfers" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -34,7 +34,7 @@ export default function TransfersListPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -66,7 +66,7 @@ export default function WastagePage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/stock" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
+1 -1
View File
@@ -130,7 +130,7 @@ export default function VendorDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/vendors" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/vendors" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
+5 -5
View File
@@ -127,7 +127,7 @@ export default function VendorsPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-foreground">Vendors</h1> <h1 className="text-2xl font-bold text-foreground">Vendors</h1>
<p className="text-base text-muted-foreground">Supplier master data code, terms, tax registration, currency (FR-MD-06).</p> <p className="text-base text-muted-foreground">Supplier master data code, terms, tax registration, currency (FR-MD-06).</p>
@@ -172,11 +172,11 @@ export default function VendorsPage() {
<FieldError errors={[errors.currency ? { message: errors.currency } : undefined]} /> <FieldError errors={[errors.currency ? { message: errors.currency } : undefined]} />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
{submitting ? "Creating…" : "Create"} {submitting ? "Creating…" : "Create"}
</Button> </Button>
</div> </div>
@@ -303,7 +303,7 @@ export default function VendorsPage() {
</div> </div>
</div> </div>
<div className="flex justify-center pt-2"> <div className="flex justify-center pt-2">
<DialogClose render={<Button variant="outline" className="min-w-36" />}>Close</DialogClose> <DialogClose render={<Button variant="outline" className="w-full sm:w-auto sm:min-w-36" />}>Close</DialogClose>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -88,7 +88,7 @@ export default function WarehouseDetailPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/warehouse" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard/warehouse" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -124,11 +124,11 @@ export default function WarehouseDetailPage() {
<Input id="bin-type" value={binType} onChange={(e) => setBinType(e.target.value)} placeholder="Shelf, Pallet, …" /> <Input id="bin-type" value={binType} onChange={(e) => setBinType(e.target.value)} placeholder="Shelf, Pallet, …" />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
{submitting ? "Creating…" : "Create"} {submitting ? "Creating…" : "Create"}
</Button> </Button>
</div> </div>
@@ -98,7 +98,7 @@ export default function WarehousesPage() {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}> <Link href="/dashboard" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
@@ -134,11 +134,11 @@ export default function WarehousesPage() {
<Input id="wh-code" value={generatedCode} readOnly disabled placeholder="Enter a name to generate a code" className="text-muted-foreground" /> <Input id="wh-code" value={generatedCode} readOnly disabled placeholder="Enter a name to generate a code" className="text-muted-foreground" />
</Field> </Field>
</FieldGroup> </FieldGroup>
<div className="flex justify-center gap-3 pt-2"> <div className="flex flex-col-reverse gap-3 pt-2 sm:flex-row sm:justify-center">
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}> <Button variant="outline" className="w-full sm:w-auto sm:min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
Cancel Cancel
</Button> </Button>
<Button className="min-w-36" onClick={handleCreate} disabled={submitting}> <Button className="w-full sm:w-auto sm:min-w-36" onClick={handleCreate} disabled={submitting}>
{submitting ? "Creating…" : "Create"} {submitting ? "Creating…" : "Create"}
</Button> </Button>
</div> </div>
+66 -2
View File
@@ -2,7 +2,7 @@
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css"; @import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *, .vibrant *));
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
@@ -129,6 +129,69 @@
--sidebar-ring: oklch(0.68 0.186 265.215); --sidebar-ring: oklch(0.68 0.186 265.215);
} }
/* Vibrant — the "System" toggle option. Light content area (background,
cards, header, table panels) paired with a dark sidebar — the same split
Linear/Vercel/Notion use in their light themes. One violet accent drives
every interactive state; the sidebar keeps its own dark token family
(applied via .sidebar-surface below) so it stays dark regardless. */
.vibrant {
--background: oklch(0.97 0.004 265);
--foreground: oklch(0.2 0.02 265);
--card: oklch(0.995 0.002 265);
--card-foreground: oklch(0.2 0.02 265);
--popover: oklch(0.995 0.002 265);
--popover-foreground: oklch(0.2 0.02 265);
--primary: oklch(0.55 0.2 275);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.93 0.02 275);
--secondary-foreground: oklch(0.32 0.15 275);
--muted: oklch(0.94 0.006 265);
--muted-foreground: oklch(0.48 0.02 265);
--accent: oklch(0.55 0.14 210);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.58 0.22 25);
--border: oklch(0.88 0.012 265);
--input: oklch(0.92 0.01 265);
--ring: oklch(0.55 0.2 275);
--success: oklch(0.55 0.15 150);
--warning: oklch(0.72 0.15 80);
--error: oklch(0.58 0.22 25);
--info: oklch(0.55 0.14 210);
--chart-1: oklch(0.55 0.2 275);
--chart-2: oklch(0.55 0.14 210);
--chart-3: oklch(0.55 0.15 150);
--chart-4: oklch(0.72 0.15 80);
--chart-5: oklch(0.58 0.22 25);
--sidebar: oklch(0.18 0.02 265);
--sidebar-foreground: oklch(0.96 0.005 265);
--sidebar-primary: oklch(0.64 0.19 275);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.28 0.03 265);
--sidebar-accent-foreground: oklch(0.96 0.005 265);
--sidebar-border: oklch(0.26 0.025 265);
--sidebar-ring: oklch(0.64 0.19 275);
}
/* Re-points the shared tokens (--card, --foreground, --muted*, --primary...)
at the sidebar's own dark family for anything inside this scope, so
AppSidebar's existing bg-card/text-foreground/text-muted-foreground
classes render dark without AppSidebar.tsx needing sidebar-specific
classes. CSS custom properties resolve against the cascade at point of
use, so this indirection (the same trick .dark/.vibrant use at the root)
works scoped to just this subtree. */
.vibrant .sidebar-surface {
--card: var(--sidebar);
--card-foreground: var(--sidebar-foreground);
--popover: var(--sidebar);
--popover-foreground: var(--sidebar-foreground);
--foreground: var(--sidebar-foreground);
--muted: var(--sidebar-accent);
--muted-foreground: oklch(0.72 0.015 265);
--primary: var(--sidebar-primary);
--primary-foreground: var(--sidebar-primary-foreground);
--border: var(--sidebar-border);
}
@layer base { @layer base {
* { * {
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
@@ -154,4 +217,5 @@
h4 { h4 {
@apply text-lg; @apply text-lg;
} }
} }
+6 -1
View File
@@ -31,7 +31,12 @@ export default function RootLayout({
suppressHydrationWarning suppressHydrationWarning
> >
<body className="min-h-full flex flex-col"> <body className="min-h-full flex flex-col">
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange> <ThemeProvider
attribute="class"
defaultTheme="light"
themes={["light", "dark", "vibrant"]}
disableTransitionOnChange
>
<TooltipProvider>{children}</TooltipProvider> <TooltipProvider>{children}</TooltipProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
@@ -25,10 +25,10 @@ export default function ForgotOtpPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
<div className="w-full max-w-2xl rounded-xl bg-card p-8 shadow-lg border border-gray-200"> <div className="w-full max-w-2xl rounded-xl bg-card p-8 shadow-lg border border-border">
<div className="flex flex-col items-center mb-8"> <div className="flex flex-col items-center mb-8">
<div className="bg-black/10 rounded-full p-4 mb-4"> <div className="bg-foreground/10 rounded-full p-4 mb-4">
<Mail className="text-black size-6" /> <Mail className="text-foreground size-6" />
</div> </div>
<h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1> <h1 className="text-2xl font-bold text-foreground text-center">Verify your email</h1>
</div> </div>
@@ -46,7 +46,7 @@ export default function ForgotOtpPage() {
e.preventDefault() e.preventDefault()
handleResend() handleResend()
}} }}
className="text-sm font-medium text-black hover:text-black/80 transition-colors" className="text-sm font-medium text-foreground hover:text-foreground/80 transition-colors"
> >
Didn't receive the code? Resend Didn't receive the code? Resend
</button> </button>
@@ -57,10 +57,10 @@ export default function ForgotOtpPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<Link href="/login" className="flex-1"> <Link href="/login" className="flex-1">
<Button variant="outline" className="w-full h-12 rounded-full border-gray-300 text-black hover:bg-black/5">Back</Button> <Button variant="outline" className="w-full h-12 rounded-full border-input text-foreground hover:bg-foreground/5">Back</Button>
</Link> </Link>
<Link href="/login/forgot/reset" className="flex-1"> <Link href="/login/forgot/reset" className="flex-1">
<Button className="w-full h-12 rounded-full bg-black text-white hover:bg-black/90">Verify</Button> <Button className="w-full h-12 rounded-full bg-foreground text-background hover:bg-foreground/90">Verify</Button>
</Link> </Link>
</div> </div>
</div> </div>
+23 -14
View File
@@ -19,7 +19,7 @@ export default function ForgotEmailPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 px-4 py-8"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 px-4 py-8">
<div className="w-full max-w-2xl rounded-xl bg-card p-8 pl-10 shadow-lg border border-gray-200"> <div className="w-full max-w-2xl rounded-xl bg-card p-8 pl-10 shadow-lg border border-border">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-bold text-foreground">Reset your password</h1> <h1 className="text-2xl font-bold text-foreground">Reset your password</h1>
<p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p> <p className="mt-2 text-sm text-muted-foreground">Enter the email address associated with your account and we'll send you a one-time code to verify your identity.</p>
@@ -40,19 +40,28 @@ export default function ForgotEmailPage() {
{emailError !== "" && <p className="text-sm text-error mt-2 font-medium">{emailError}</p>} {emailError !== "" && <p className="text-sm text-error mt-2 font-medium">{emailError}</p>}
</div> </div>
<Button <div className="flex gap-3">
className="w-full h-12 text-base font-medium rounded-full bg-black text-white hover:bg-black/90" <Button
disabled={emailError !== ""} variant="outline"
onClick={() => { onClick={() => router.push('/login')}
if (email.length === 0) { className="flex-1 h-12 rounded-full border-input text-foreground hover:bg-foreground/5"
setEmailError('Email is required') >
return Back
} </Button>
if (emailError === "") router.push('/login/forgot/otp') <Button
}} className="flex-1 h-12 text-base font-medium rounded-full bg-foreground text-background hover:bg-foreground/90"
> disabled={emailError !== ""}
Send verification code onClick={() => {
</Button> if (email.length === 0) {
setEmailError('Email is required')
return
}
if (emailError === "") router.push('/login/forgot/otp')
}}
>
Send verification code
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -39,7 +39,7 @@ export default function ForgotResetPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background to-background/95 p-4">
<div className="w-full max-w-xl rounded-xl bg-card p-8 shadow-lg border border-gray-200"> <div className="w-full max-w-xl rounded-xl bg-card p-8 shadow-lg border border-border">
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<h1 className="text-2xl font-bold text-foreground">Create a new password</h1> <h1 className="text-2xl font-bold text-foreground">Create a new password</h1>
<p className="mt-2 text-sm text-muted-foreground">Choose a strong password to secure your account.</p> <p className="mt-2 text-sm text-muted-foreground">Choose a strong password to secure your account.</p>
@@ -103,15 +103,15 @@ export default function ForgotResetPage() {
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
variant="outline" variant="outline"
onClick={() => router.back()} onClick={() => router.push('/login')}
className="flex-1 h-12 rounded-full border-gray-300 text-black hover:bg-black/5" className="flex-1 h-12 rounded-full border-input text-foreground hover:bg-foreground/5"
> >
Back Back
</Button> </Button>
<Button <Button
disabled={!allValid} disabled={!allValid}
onClick={() => setShowSuccess(true)} onClick={() => setShowSuccess(true)}
className="flex-1 h-12 rounded-full bg-black text-white hover:bg-black/90" className="flex-1 h-12 rounded-full bg-foreground text-background hover:bg-foreground/90"
> >
Change password Change password
</Button> </Button>
@@ -131,7 +131,7 @@ export default function ForgotResetPage() {
</div> </div>
</div> </div>
</DialogHeader> </DialogHeader>
<Button onClick={() => router.push('/login')} className="w-4/5 mx-auto h-12 mt-6 rounded-full bg-black text-white hover:bg-black/90"> <Button onClick={() => router.push('/login')} className="w-4/5 mx-auto h-12 mt-6 rounded-full bg-foreground text-background hover:bg-foreground/90">
Return to sign in Return to sign in
</Button> </Button>
</DialogContent> </DialogContent>
+16 -48
View File
@@ -17,29 +17,6 @@ import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
function GoogleIcon() {
return (
<svg viewBox="0 0 24 24" className="size-4">
<path
fill="#4285F4"
d="M23.52 12.27c0-.85-.08-1.67-.22-2.45H12v4.64h6.47a5.53 5.53 0 0 1-2.4 3.63v3h3.89c2.27-2.09 3.56-5.17 3.56-8.82Z"
/>
<path
fill="#34A853"
d="M12 24c3.24 0 5.95-1.07 7.93-2.91l-3.89-3c-1.08.73-2.46 1.15-4.04 1.15-3.1 0-5.73-2.09-6.67-4.9H1.32v3.09A12 12 0 0 0 12 24Z"
/>
<path
fill="#FBBC05"
d="M5.33 14.34a7.2 7.2 0 0 1 0-4.68V6.57H1.32a12 12 0 0 0 0 10.86l4.01-3.09Z"
/>
<path
fill="#EA4335"
d="M12 4.77c1.76 0 3.35.6 4.6 1.79l3.45-3.45C17.94 1.19 15.23 0 12 0A12 12 0 0 0 1.32 6.57l4.01 3.09C6.27 6.86 8.9 4.77 12 4.77Z"
/>
</svg>
)
}
export default function LoginPage() { export default function LoginPage() {
return ( return (
<Suspense fallback={null}> <Suspense fallback={null}>
@@ -77,19 +54,28 @@ function LoginForm() {
return ( return (
<div className="relative min-h-screen overflow-hidden"> <div className="relative min-h-screen overflow-hidden">
{/* Full-page background photo — right side only */} {/* Full-page background photo — right side only. Dark mode swaps to the backdrop-free
<div className="absolute inset-y-0 left-[45%] right-0"> cutout (whithoutbackground.png) so there's no white scene box floating on the
dark panel. */}
<div className="absolute inset-y-0 left-[45%] right-0 bg-white dark:bg-background">
<Image <Image
src="/images/loginerps.png" src="/images/loginerps.png"
alt="ERP Illustration" alt="ERP Illustration"
fill fill
className="object-contain object-center bg-white" className="object-contain object-center dark:hidden"
priority
/>
<Image
src="/images/whithoutbackground.png"
alt="ERP Illustration"
fill
className="hidden object-contain object-center dark:block"
priority priority
/> />
</div> </div>
{/* Login panel — overlays left side, no hard border with the photo */} {/* Login panel — overlays left side, no hard border with the photo */}
<main className="relative z-10 flex min-h-screen w-full flex-col justify-center bg-white px-6 py-12 sm:px-12 md:w-[45%]"> <main className="relative z-10 flex min-h-screen w-full flex-col justify-center bg-white dark:bg-background px-6 py-12 sm:px-12 md:w-[45%]">
<div className="mb-8 flex justify-center"> <div className="mb-8 flex justify-center">
<Link href="/" className="inline-flex items-center"> <Link href="/" className="inline-flex items-center">
<Image src="/images/logo%20(2).png" alt="Hexa ERP" width={300} height={150} /> <Image src="/images/logo%20(2).png" alt="Hexa ERP" width={300} height={150} />
@@ -153,7 +139,7 @@ function LoginForm() {
</div> </div>
<FieldError errors={[form.formState.errors.password]} /> <FieldError errors={[form.formState.errors.password]} />
<div className="flex justify-end mt-1"> <div className="flex justify-end mt-1">
<Link href="/login/forgot" className="text-sm font-medium text-black hover:text-black/70 transition-colors"> <Link href="/login/forgot" className="text-sm font-medium text-foreground hover:text-foreground/70 transition-colors">
Forgot password? Forgot password?
</Link> </Link>
</div> </div>
@@ -168,7 +154,7 @@ function LoginForm() {
<Button <Button
type="submit" type="submit"
className="w-1/2 mx-auto mt-8 h-12 text-sm font-medium rounded-full bg-black text-white hover:bg-black/90" className="w-1/2 mx-auto mt-8 h-12 text-sm font-medium rounded-full bg-foreground text-background hover:bg-foreground/90"
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
> >
{form.formState.isSubmitting ? "Signing in…" : "Sign in"} {form.formState.isSubmitting ? "Signing in…" : "Sign in"}
@@ -181,27 +167,9 @@ function LoginForm() {
{Object.values(form.formState.errors).length > 0 && "There are errors in the form."} {Object.values(form.formState.errors).length > 0 && "There are errors in the form."}
</div> </div>
<div className="my-8 flex items-center gap-4">
<div className="flex-1 h-px bg-border" />
<span className="text-sm text-muted-foreground font-medium">Or continue with</span>
<div className="flex-1 h-px bg-border" />
</div>
<Button
type="button"
className="w-full h-11 text-base font-medium inline-flex items-center justify-center gap-3 rounded-full bg-transparent text-foreground border border-gray-300 hover:bg-black/5"
onClick={() => {
alert('Google OAuth flow placeholder')
}}
aria-label="Continue with Google"
>
<GoogleIcon />
Continue with Google
</Button>
<div className="mt-12 text-center"> <div className="mt-12 text-center">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Don&apos;t have an account? <Link href="#" className="font-semibold text-black hover:text-black/80 transition-colors">Create an account</Link> Don&apos;t have an account? <Link href="#" className="font-semibold text-foreground hover:text-foreground/80 transition-colors">Create an account</Link>
</p> </p>
</div> </div>
</main> </main>
@@ -47,6 +47,9 @@ const navItems: {
/** Where clicking the row actually navigates, if different from `href`. `href` itself /** Where clicking the row actually navigates, if different from `href`. `href` itself
* stays the section prefix used to decide whether this row is "active". */ * stays the section prefix used to decide whether this row is "active". */
landingHref?: string landingHref?: string
/** This item's own href has no page of its own (no landingHref either) — clicking the
* row should only expand/collapse its children, never navigate. */
expandOnly?: boolean
icon: LucideIcon icon: LucideIcon
chevron?: boolean chevron?: boolean
children?: { title: string; code: string; href: string; icon: LucideIcon }[] children?: { title: string; code: string; href: string; icon: LucideIcon }[]
@@ -106,6 +109,7 @@ const navItems: {
title: "Settings", title: "Settings",
code: "settings", code: "settings",
href: "/dashboard/settings", href: "/dashboard/settings",
expandOnly: true,
icon: Settings, icon: Settings,
chevron: true, chevron: true,
children: [ children: [
@@ -139,9 +143,13 @@ function SidebarContent({
const [expanded, setExpanded] = useState<Record<string, boolean>>({}) const [expanded, setExpanded] = useState<Record<string, boolean>>({})
useEffect(() => { useEffect(() => {
const parent = items.find((i) => const parent = items.find((i) => {
i.children?.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`)) if (!i.children?.length) return false
) if (i.children.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))) return true
// Landing on the parent's own hub page (e.g. /dashboard/settings itself,
// not one of its children) should also reveal its sub-items.
return pathname === i.href || pathname.startsWith(`${i.href}/`)
})
if (parent) { if (parent) {
setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true })) setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true }))
} }
@@ -153,7 +161,7 @@ function SidebarContent({
return ( return (
<nav <nav
className={cn( className={cn(
"flex h-full flex-col rounded-3xl bg-card p-3 shadow-sm ring-1 ring-foreground/10 transition-[width] duration-300 ease-in-out", "sidebar-surface flex h-full flex-col rounded-3xl bg-card p-3 shadow-sm ring-1 ring-foreground/10 transition-[width] duration-300 ease-in-out",
!isMobile && (collapsed ? "w-20" : "w-64") !isMobile && (collapsed ? "w-20" : "w-64")
)} )}
> >
@@ -187,7 +195,7 @@ function SidebarContent({
{/* Nav items — scrolls internally when it overflows, without a visible {/* Nav items — scrolls internally when it overflows, without a visible
scrollbar so the rounded panel stays clean. */} scrollbar so the rounded panel stays clean. */}
<ul className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"> <ul className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{items.map((item) => { {items.map((item) => {
const isActive = const isActive =
item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href) item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href)
@@ -205,15 +213,18 @@ function SidebarContent({
<Link <Link
href={item.landingHref ?? item.href} href={item.landingHref ?? item.href}
title={iconOnly ? item.title : undefined} title={iconOnly ? item.title : undefined}
onClick={onClose} onClick={() => {
onClose?.()
if (hasChildren) setExpanded((prev) => (prev[item.code] ? prev : { ...prev, [item.code]: true }))
}}
className={cn( className={cn(
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-3 text-base font-semibold", "flex flex-1 items-center gap-3 rounded-2xl px-4 py-1.5 text-base font-normal",
iconOnly && "justify-center px-0", iconOnly && "justify-center px-0",
isActive ? "text-primary" : "text-foreground" isActive ? "text-primary" : "text-foreground"
)} )}
> >
<item.icon <item.icon
className={cn("size-5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")} className={cn("size-3.5 shrink-0", isActive ? "text-primary" : "text-muted-foreground")}
/> />
{!iconOnly && ( {!iconOnly && (
<> <>
@@ -270,14 +281,14 @@ function SidebarContent({
onClick={onClose} onClick={onClose}
tabIndex={isOpen ? undefined : -1} tabIndex={isOpen ? undefined : -1}
className={cn( className={cn(
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-colors", "flex items-center gap-2.5 rounded-xl px-3 py-1 text-sm font-medium transition-colors",
childActive childActive
? "bg-primary/10 text-primary" ? "bg-primary/10 text-primary"
: "text-muted-foreground hover:bg-muted hover:text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground"
)} )}
> >
<child.icon <child.icon
className={cn("size-4 shrink-0", childActive ? "text-primary" : "text-muted-foreground")} className={cn("size-3 shrink-0", childActive ? "text-primary" : "text-muted-foreground")}
/> />
{child.title} {child.title}
</Link> </Link>
@@ -362,9 +373,9 @@ export function AppSidebar() {
type="button" type="button"
onClick={() => setMobileOpen(true)} onClick={() => setMobileOpen(true)}
aria-label="Open menu" aria-label="Open menu"
className="fixed top-5 left-5 z-40 flex size-10 items-center justify-center rounded-2xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden" className="fixed top-8 left-5 z-40 flex size-8 items-center justify-center rounded-xl bg-card shadow-sm ring-1 ring-foreground/10 text-muted-foreground hover:bg-muted lg:hidden"
> >
<Menu className="size-5" /> <Menu className="size-4" />
</button> </button>
{/* ── Mobile overlay backdrop ──────────────────────── */} {/* ── Mobile overlay backdrop ──────────────────────── */}
@@ -164,8 +164,8 @@ export function Header() {
} }
return ( return (
<header className="mx-6 mt-3 mb-6 flex items-center justify-between gap-2 rounded-3xl bg-card py-4 pr-4 pl-14 shadow-sm ring-1 ring-foreground/10 lg:mx-8 lg:mt-4 lg:gap-4 lg:p-4"> <header className="mx-3 mt-3 mb-3 flex items-center justify-between gap-2 rounded-3xl bg-card py-4 pr-3 pl-12 shadow-sm ring-1 ring-foreground/10 sm:mx-6 sm:pr-4 sm:pl-14 lg:mx-8 lg:mt-4 lg:mb-4 lg:gap-4 lg:p-4">
<div className="flex items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
{showBackButton && ( {showBackButton && (
<button <button
type="button" type="button"
@@ -176,10 +176,10 @@ export function Header() {
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
</button> </button>
)} )}
<h1 className="text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1> <h1 className="min-w-0 truncate text-base font-bold tracking-tight text-foreground sm:text-xl">{title}</h1>
</div> </div>
<div className="flex items-center gap-2 sm:gap-3"> <div className="flex shrink-0 items-center gap-1.5 sm:gap-3">
<ThemeToggle /> <ThemeToggle />
<Popover> <Popover>
+42 -18
View File
@@ -1,38 +1,62 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Moon, Sun } from "lucide-react" import { Moon, Sun, Monitor } from "lucide-react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const options = [
{ value: "light", label: "Light mode", icon: Sun },
{ value: "vibrant", label: "Vibrant theme", icon: Monitor },
{ value: "dark", label: "Dark mode", icon: Moon },
] as const
/** /**
* Renders an empty slot until mounted: `resolvedTheme` is unknown on the server (and on * Renders an empty slot until mounted: `theme` is unknown on the server (and on the
* the client's first paint, before next-themes reads localStorage), so rendering an icon * client's first paint, before next-themes reads localStorage), so rendering the active
* before that would either be wrong or cause a hydration mismatch. * segment before that would either be wrong or cause a hydration mismatch.
*/ */
export function ThemeToggle({ className }: { className?: string }) { export function ThemeToggle({ className }: { className?: string }) {
const { resolvedTheme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), []) useEffect(() => setMounted(true), [])
if (!mounted) { if (!mounted) {
return <div className={cn("size-10 shrink-0", className)} aria-hidden="true" /> return <div className={cn("h-10 w-27 shrink-0", className)} aria-hidden="true" />
} }
const isDark = resolvedTheme === "dark"
return ( return (
<button <div
type="button" role="radiogroup"
onClick={() => setTheme(isDark ? "light" : "dark")} aria-label="Theme"
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"} className={cn("flex shrink-0 items-center gap-0.5 rounded-full bg-muted p-1", className)}
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground",
className
)}
> >
{isDark ? <Sun className="size-5" /> : <Moon className="size-5" />} {options.map(({ value, label, icon: Icon }) => {
</button> const isActive = theme === value
const isVibrant = value === "vibrant"
return (
<button
key={value}
type="button"
role="radio"
aria-checked={isActive}
aria-label={label}
title={label}
onClick={() => setTheme(value)}
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full transition-colors",
isActive
? isVibrant
? "bg-linear-to-br from-indigo-500 to-violet-600 text-white shadow-sm"
: "bg-card text-primary shadow-sm ring-1 ring-foreground/10"
: "text-muted-foreground hover:text-foreground"
)}
>
<Icon className="size-4" />
</button>
)
})}
</div>
) )
} }
@@ -143,12 +143,11 @@ export function DataTable<T>({
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-indigo-50 hover:bg-indigo-50"> <TableRow className="border-b-0 hover:bg-primary/5">
{columns.map((column, index) => ( {columns.map((column, index) => (
<TableHead <TableHead
key={column.key} key={column.key}
className={cn( className={cn(
"text-indigo-700",
index === 0 && "rounded-l-lg pl-3", index === 0 && "rounded-l-lg pl-3",
index === columns.length - 1 && !actions && "rounded-r-lg pr-3", index === columns.length - 1 && !actions && "rounded-r-lg pr-3",
column.headerClassName column.headerClassName
@@ -158,7 +157,7 @@ export function DataTable<T>({
</TableHead> </TableHead>
))} ))}
{actions && ( {actions && (
<TableHead className="rounded-r-lg pr-3 text-right text-indigo-700"> <TableHead className="rounded-r-lg pr-3 text-right">
Actions Actions
</TableHead> </TableHead>
)} )}
+2 -2
View File
@@ -23,7 +23,7 @@ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return ( return (
<thead <thead
data-slot="table-header" data-slot="table-header"
className={cn("[&_tr]:border-b", className)} className={cn("bg-primary/5 [&_tr]:border-b [&_tr]:hover:bg-primary/5", className)}
{...props} {...props}
/> />
) )
@@ -70,7 +70,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th <th
data-slot="table-head" data-slot="table-head"
className={cn( className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0", "h-10 px-2 text-left align-middle font-semibold whitespace-nowrap text-primary [&:has([role=checkbox])]:pr-0",
className className
)} )}
{...props} {...props}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB