Builds on the env-var INTEGRATIONS_DISABLED shipped with the nav-design overhaul. Adds a DB-backed admin UI so operators can flip integrations without editing .env and restarting the container, plus the remaining visual cleanup items from the design backlog. Integration toggles - Migration 081 — integration_settings table (key PK, disabled flag, reason, disabled_by audit, disabled_at). Seeded with all 13 known integrations as enabled. - GET / PATCH /api/admin/integrations — gated by requirePermission (admin, access). PATCH clears the in-process integration-health cache so toggles take effect within seconds. - /admin/integrations admin page with a Switch per integration, optional reason input, audit-info subtitle (disabled by, when, why), live status light from /api/dashboard/integration-health. - integration-health service merges env-var disable list with DB rows; degrades gracefully if migration unapplied / DB unreachable. - Wired into the Admin nav dropdown (eight items now). - CLAUDE.md describes both env + DB sources. Sticky first column on tables - Table primitive accepts stickyFirstColumn?: boolean. When true, TH and TD :first-child stay pinned during horizontal scroll, with background inheritance preserving hover and selected row tints. - DataTable exposes the prop too — on by default for paginated tables. - /addigy-devices opts in. Dark-mode contrast - --border lifted from 10% to 14% in .dark; --input from 15% to 18%; --sidebar-border to 14%. - StatusLight outline ring lifted from /10 to /15 (light) and /20 (dark). - DetailModal empty-cell em-dash lifted from /40 to /70 so missing values are legible on dark surfaces. DESIGN.md - Closed sticky-first-column, dark-mode contrast, and palette-audit items (palette deprioritized — most uses are semantic). - Skeleton helpers documented as preferred for new code; existing ad-hoc patterns left in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
3.5 KiB
TypeScript
141 lines
3.5 KiB
TypeScript
"use client"
|
|
|
|
import * as React from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface TableProps extends React.ComponentProps<"table"> {
|
|
/** Pin the first column when the table scrolls horizontally. Useful on
|
|
* wide list tables where the first column is an identifier. */
|
|
stickyFirstColumn?: boolean
|
|
}
|
|
|
|
function Table({ className, stickyFirstColumn, ...props }: TableProps) {
|
|
return (
|
|
<div
|
|
data-slot="table-container"
|
|
className="relative w-full overflow-x-auto"
|
|
>
|
|
<table
|
|
data-slot="table"
|
|
className={cn(
|
|
"w-full caption-bottom text-sm",
|
|
stickyFirstColumn && [
|
|
// Header cell: sticky against the muted header bg.
|
|
"[&_thead_tr_th:first-child]:sticky",
|
|
"[&_thead_tr_th:first-child]:left-0",
|
|
"[&_thead_tr_th:first-child]:z-20",
|
|
"[&_thead_tr_th:first-child]:bg-muted",
|
|
// Body cell: sticky, inherits the row's background so hover +
|
|
// selected states still read; falls back to card surface.
|
|
"[&_tbody_tr_td:first-child]:sticky",
|
|
"[&_tbody_tr_td:first-child]:left-0",
|
|
"[&_tbody_tr_td:first-child]:z-10",
|
|
"[&_tbody_tr_td:first-child]:bg-card",
|
|
// When the parent row has its hover bg, override.
|
|
"[&_tbody_tr:hover_td:first-child]:bg-muted/50",
|
|
"[&_tbody_tr[data-state=selected]_td:first-child]:bg-muted",
|
|
],
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
|
return (
|
|
<thead
|
|
data-slot="table-header"
|
|
className={cn("[&_tr]:border-b", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
|
return (
|
|
<tbody
|
|
data-slot="table-body"
|
|
className={cn("[&_tr:last-child]:border-0", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
|
return (
|
|
<tfoot
|
|
data-slot="table-footer"
|
|
className={cn(
|
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
return (
|
|
<tr
|
|
data-slot="table-row"
|
|
className={cn(
|
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
return (
|
|
<th
|
|
data-slot="table-head"
|
|
className={cn(
|
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
|
return (
|
|
<td
|
|
data-slot="table-cell"
|
|
className={cn(
|
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function TableCaption({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<"caption">) {
|
|
return (
|
|
<caption
|
|
data-slot="table-caption"
|
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export {
|
|
Table,
|
|
TableHeader,
|
|
TableBody,
|
|
TableFooter,
|
|
TableHead,
|
|
TableRow,
|
|
TableCell,
|
|
TableCaption,
|
|
}
|