@exxatdesignux/ui 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/consumer-extras/AGENTS.md +76 -0
- package/consumer-extras/README.md +5 -1
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
- package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
- package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
- package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
- package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
- package/consumer-extras/patterns/data-views-pattern.md +40 -3
- package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
- package/package.json +2 -1
- package/src/components/ui/button-group.tsx +81 -0
- package/src/components/ui/button.tsx +4 -4
- package/src/globals.css +7 -1858
- package/src/theme.css +10 -1126
- package/src/tokens/README.md +15 -0
- package/src/tokens/base.css +337 -0
- package/src/tokens/high-contrast.css +1195 -0
- package/src/tokens/layers.css +224 -0
- package/src/tokens/tailwind-bridge.css +118 -0
- package/src/tokens/themes.css +201 -0
- package/template/AGENTS.md +60 -22
- package/template/app/(app)/dashboard/loading.tsx +3 -15
- package/template/app/(app)/dashboard/page.tsx +2 -14
- package/template/app/(app)/data-list/layout.tsx +43 -0
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
- package/template/app/(app)/examples/page.tsx +1 -0
- package/template/app/(app)/loading.tsx +1 -18
- package/template/app/(app)/question-bank/find/page.tsx +2 -1
- package/template/app/(app)/question-bank/library/page.tsx +2 -1
- package/template/app/(app)/question-bank/list/page.tsx +2 -1
- package/template/app/(app)/question-bank/new/page.tsx +15 -23
- package/template/app/(app)/question-bank/page.tsx +2 -1
- package/template/app/(app)/settings/page.tsx +4 -5
- package/template/app/globals.css +7 -1964
- package/template/components/app-route-loading.tsx +14 -0
- package/template/components/app-sidebar.tsx +70 -55
- package/template/components/data-views/index.ts +37 -9
- package/template/components/data-views/list-page-calendar-view.tsx +593 -0
- package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
- package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
- package/template/components/examples/focused-workflow-showcase.tsx +183 -0
- package/template/components/list-hub-board-view.tsx +68 -0
- package/template/components/list-hub-client.tsx +186 -0
- package/template/components/list-hub-list-view.tsx +36 -0
- package/template/components/list-hub-panel-activator.tsx +8 -0
- package/template/components/list-hub-secondary-nav.tsx +121 -0
- package/template/components/list-hub-table.tsx +336 -0
- package/template/components/new-question-composer.tsx +6 -24
- package/template/components/product-switcher.tsx +3 -2
- package/template/components/question-bank-client.tsx +4 -1
- package/template/components/question-bank-folder-columns-panel.tsx +104 -0
- package/template/components/question-bank-table.tsx +143 -485
- package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
- package/template/components/secondary-panel.tsx +4 -44
- package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
- package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
- package/template/components/secondary-panels/registry.tsx +15 -0
- package/template/components/settings-appearance-card.tsx +3 -2
- package/template/components/settings-client.tsx +59 -15
- package/template/components/settings-form-row.tsx +9 -4
- package/template/components/table-properties/drawer-button.tsx +13 -0
- package/template/components/table-properties/drawer.tsx +65 -4
- package/template/components/templates/focused-workflow-layouts.tsx +448 -0
- package/template/components/templates/focused-workflow-page-template.tsx +69 -0
- package/template/components/templates/list-page.tsx +29 -5
- package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
- package/template/components/templates/page-loading-shell.tsx +262 -0
- package/template/components/ui/button-group.tsx +1 -0
- package/template/docs/consumer-app-pattern.md +39 -0
- package/template/docs/data-views-pattern.md +40 -3
- package/template/docs/drawer-vs-dialog-pattern.md +3 -1
- package/template/docs/focused-workflow-page-pattern.md +84 -0
- package/template/docs/shell-surface-elevation-pattern.md +5 -3
- package/template/lib/command-menu-search-data.ts +11 -27
- package/template/lib/data-list-display-options.ts +16 -2
- package/template/lib/data-list-view-registry.ts +104 -0
- package/template/lib/data-list-view-surface.ts +15 -1
- package/template/lib/data-list-view.ts +10 -1
- package/template/lib/data-view-dashboard-storage.ts +38 -35
- package/template/lib/hub-connected-view-renderers.ts +58 -0
- package/template/lib/list-hub-nav.ts +121 -0
- package/template/lib/list-hub-supported-views.ts +10 -0
- package/template/lib/list-page-table-properties.ts +3 -7
- package/template/lib/list-status-badges.ts +4 -97
- package/template/lib/mock/list-hub-directory.ts +27 -0
- package/template/lib/mock/list-hub-kpi.ts +27 -0
- package/template/lib/mock/navigation.tsx +1 -0
- package/template/lib/page-loading-variant.ts +40 -0
- package/template/lib/question-bank-supported-views.ts +13 -0
- package/template/lib/table-state-lifecycle.ts +2 -2
- package/template/app/(app)/data-list/[id]/page.tsx +0 -44
- package/template/app/(app)/data-list/new/page.tsx +0 -34
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-list-view.tsx +0 -54
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -612
- package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
- package/template/components/data-view-dashboard-charts-team.tsx +0 -971
- package/template/components/data-view-dashboard-charts.tsx +0 -1503
- package/template/components/new-placement-back-btn.tsx +0 -28
- package/template/components/new-placement-form.tsx +0 -1068
- package/template/components/placement-board-card.tsx +0 -262
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -404
- package/template/components/placements-client.tsx +0 -252
- package/template/components/placements-list-view.tsx +0 -171
- package/template/components/placements-page-header.tsx +0 -166
- package/template/components/placements-table-cells.test.tsx +0 -22
- package/template/components/placements-table-cells.tsx +0 -173
- package/template/components/placements-table-columns.tsx +0 -640
- package/template/components/placements-table.tsx +0 -1642
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-all-client.tsx +0 -154
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-list-view.tsx +0 -42
- package/template/components/sites-table.tsx +0 -382
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-list-view.tsx +0 -59
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -693
- package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
- package/template/lib/mock/compliance-kpi.ts +0 -61
- package/template/lib/mock/compliance.ts +0 -146
- package/template/lib/mock/placements-kpi.ts +0 -134
- package/template/lib/mock/placements.ts +0 -183
- package/template/lib/mock/sites-directory.ts +0 -16
- package/template/lib/mock/sites-kpi.ts +0 -25
- package/template/lib/mock/team-kpi.ts +0 -60
- package/template/lib/mock/team.ts +0 -118
- package/template/lib/placement-board-card-layout.ts +0 -79
- package/template/lib/placement-lifecycle.ts +0 -5
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MetricInsight, MetricItem } from "@/components/key-metrics"
|
|
2
|
+
import type { ListHubRecord } from "@/lib/mock/list-hub-directory"
|
|
3
|
+
|
|
4
|
+
export function listHubKpiMetrics(countOrRows: number | ListHubRecord[]): MetricItem[] {
|
|
5
|
+
const count = typeof countOrRows === "number" ? countOrRows : countOrRows.length
|
|
6
|
+
const scheduled = Math.max(0, count - 2)
|
|
7
|
+
const thisWeek = Math.min(count, 4)
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
id: "total",
|
|
11
|
+
label: "Total records",
|
|
12
|
+
value: count,
|
|
13
|
+
delta: "+2",
|
|
14
|
+
trend: "up",
|
|
15
|
+
href: "#",
|
|
16
|
+
metricVariant: "hero",
|
|
17
|
+
},
|
|
18
|
+
{ id: "scheduled", label: "Scheduled", value: scheduled, delta: "+1", trend: "up", href: "#" },
|
|
19
|
+
{ id: "this-week", label: "This week", value: thisWeek, delta: "—", trend: "neutral", href: "#" },
|
|
20
|
+
{ id: "completed", label: "Completed", value: 2, delta: "—", trend: "neutral", href: "#" },
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const LIST_HUB_KPI_INSIGHT: MetricInsight = {
|
|
25
|
+
title: "3 events land this week",
|
|
26
|
+
description: "Filtered calendar and table views share the same row set after search and filters.",
|
|
27
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps app routes to loading skeleton layouts — keep in sync with page templates
|
|
3
|
+
* (`PrimaryPageTemplate`, `FocusedWorkflowPageTemplate`, dedicated search, hub landing).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type PageLoadingVariant =
|
|
7
|
+
| "dashboard"
|
|
8
|
+
| "primary-list-hub"
|
|
9
|
+
| "question-bank-hub"
|
|
10
|
+
| "dedicated-search"
|
|
11
|
+
| "focused-workflow"
|
|
12
|
+
| "focused-workflow-sidebar"
|
|
13
|
+
| "simple"
|
|
14
|
+
|
|
15
|
+
function normalizePathname(pathname: string): string {
|
|
16
|
+
if (!pathname || pathname === "/") return "/"
|
|
17
|
+
return pathname.length > 1 && pathname.endsWith("/") ? pathname.slice(0, -1) : pathname
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Pick the loading skeleton for the destination route (client-safe). */
|
|
21
|
+
export function resolvePageLoadingVariant(pathname: string): PageLoadingVariant {
|
|
22
|
+
const path = normalizePathname(pathname)
|
|
23
|
+
|
|
24
|
+
if (path === "/dashboard") return "dashboard"
|
|
25
|
+
if (path === "/settings") return "focused-workflow-sidebar"
|
|
26
|
+
if (path === "/help" || path === "/examples" || path.startsWith("/examples/")) {
|
|
27
|
+
return path === "/examples/focused-workflow" ? "focused-workflow-sidebar" : "simple"
|
|
28
|
+
}
|
|
29
|
+
if (path === "/question-bank/new" || path.startsWith("/question-bank/new/")) {
|
|
30
|
+
return "focused-workflow"
|
|
31
|
+
}
|
|
32
|
+
if (path === "/question-bank/find" || path === "/question-bank/list") {
|
|
33
|
+
return "dedicated-search"
|
|
34
|
+
}
|
|
35
|
+
if (path === "/question-bank") return "question-bank-hub"
|
|
36
|
+
if (path.startsWith("/question-bank/")) return "primary-list-hub"
|
|
37
|
+
if (path === "/data-list") return "primary-list-hub"
|
|
38
|
+
|
|
39
|
+
return "primary-list-hub"
|
|
40
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DataListViewType } from "@/lib/data-list-view"
|
|
2
|
+
|
|
3
|
+
/** Views implemented in `QuestionBankTable` — keep in sync with `ListPageConnectedViewBody` renderers. */
|
|
4
|
+
export const QUESTION_BANK_SUPPORTED_VIEWS = [
|
|
5
|
+
"table",
|
|
6
|
+
"list",
|
|
7
|
+
"board",
|
|
8
|
+
"dashboard",
|
|
9
|
+
"calendar",
|
|
10
|
+
"folder",
|
|
11
|
+
"panel",
|
|
12
|
+
"tree-panel",
|
|
13
|
+
] as const satisfies readonly DataListViewType[]
|
|
@@ -26,7 +26,7 @@ import type { RowHeight } from "@/lib/row-height"
|
|
|
26
26
|
import type { DataListDisplayOptions } from "@/lib/data-list-display-options"
|
|
27
27
|
import type { ActiveFilter, ConditionalRule, SortRule } from "@/components/table-properties/types"
|
|
28
28
|
import type { ViewTab } from "@/components/templates/list-page"
|
|
29
|
-
import type
|
|
29
|
+
import { DATA_LIST_VIEW_TILES, type DataListViewType } from "@/lib/data-list-view"
|
|
30
30
|
|
|
31
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
32
|
// Storage key + debounce config
|
|
@@ -139,7 +139,7 @@ export interface TableStatePersistSlice {
|
|
|
139
139
|
// Parsers + validators
|
|
140
140
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
141
|
|
|
142
|
-
const VIEW_TYPES: DataListViewType[] =
|
|
142
|
+
const VIEW_TYPES: DataListViewType[] = DATA_LIST_VIEW_TILES.map(t => t.value)
|
|
143
143
|
|
|
144
144
|
function isViewType(v: unknown): v is DataListViewType {
|
|
145
145
|
return typeof v === "string" && (VIEW_TYPES as string[]).includes(v)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import Link from "next/link"
|
|
2
|
-
import { notFound } from "next/navigation"
|
|
3
|
-
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
4
|
-
import { Button } from "@/components/ui/button"
|
|
5
|
-
import { getPlacementById } from "@/lib/mock/placements"
|
|
6
|
-
|
|
7
|
-
export default async function RecordDetailPage({
|
|
8
|
-
params,
|
|
9
|
-
}: {
|
|
10
|
-
params: Promise<{ id: string }>
|
|
11
|
-
}) {
|
|
12
|
-
const { id } = await params
|
|
13
|
-
const row = getPlacementById(Number(id))
|
|
14
|
-
if (!row) notFound()
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<PrimaryPageTemplate
|
|
18
|
-
siteHeader={{
|
|
19
|
-
title: "Record",
|
|
20
|
-
breadcrumbs: [{ label: "List hub", href: "/data-list" }],
|
|
21
|
-
}}
|
|
22
|
-
maxWidthClassName="max-w-2xl"
|
|
23
|
-
contentClassName="px-4 lg:px-6 py-6"
|
|
24
|
-
bodyClassName="overflow-y-auto"
|
|
25
|
-
>
|
|
26
|
-
<p className="text-sm text-muted-foreground mb-6">
|
|
27
|
-
Demo read-only detail — replace with your domain route and data fetch.
|
|
28
|
-
</p>
|
|
29
|
-
<dl className="grid gap-3 text-sm sm:grid-cols-[minmax(0,10rem)_1fr]">
|
|
30
|
-
<dt className="text-muted-foreground">Primary label</dt>
|
|
31
|
-
<dd className="font-medium text-foreground">{row.student}</dd>
|
|
32
|
-
<dt className="text-muted-foreground">Status</dt>
|
|
33
|
-
<dd>{row.status}</dd>
|
|
34
|
-
<dt className="text-muted-foreground">Site</dt>
|
|
35
|
-
<dd>{row.site}</dd>
|
|
36
|
-
<dt className="text-muted-foreground">Program</dt>
|
|
37
|
-
<dd>{row.program}</dd>
|
|
38
|
-
</dl>
|
|
39
|
-
<Button asChild variant="outline" className="mt-8">
|
|
40
|
-
<Link href="/data-list">Back to list</Link>
|
|
41
|
-
</Button>
|
|
42
|
-
</PrimaryPageTemplate>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import Link from "next/link"
|
|
2
|
-
import { NewPlacementForm } from "@/components/new-placement-form"
|
|
3
|
-
import { SidebarAutoCollapse } from "@/components/sidebar-auto-collapse"
|
|
4
|
-
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
5
|
-
|
|
6
|
-
export default function NewRecordPage() {
|
|
7
|
-
return (
|
|
8
|
-
<PrimaryPageTemplate
|
|
9
|
-
beforeSiteHeader={<SidebarAutoCollapse />}
|
|
10
|
-
bodyClassName="overflow-y-auto"
|
|
11
|
-
maxWidthClassName="max-w-3xl"
|
|
12
|
-
contentClassName="px-8 pt-10 pb-32"
|
|
13
|
-
>
|
|
14
|
-
<Link
|
|
15
|
-
href="/data-list"
|
|
16
|
-
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-interactive-hover-foreground transition-colors mb-5 group"
|
|
17
|
-
aria-label="Back to list hub"
|
|
18
|
-
>
|
|
19
|
-
<i className="fa-light fa-arrow-left text-xs transition-transform group-hover:-translate-x-0.5" aria-hidden="true" />
|
|
20
|
-
Back
|
|
21
|
-
</Link>
|
|
22
|
-
<h1
|
|
23
|
-
className="text-[2.25rem] font-semibold tracking-tight leading-none text-foreground mb-2"
|
|
24
|
-
style={{ fontFamily: "var(--font-heading)" }}
|
|
25
|
-
>
|
|
26
|
-
New record
|
|
27
|
-
</h1>
|
|
28
|
-
<p className="text-sm text-muted-foreground mb-8">
|
|
29
|
-
Multi-step wizard shell (demo fields) — swap the form for your product flow.
|
|
30
|
-
</p>
|
|
31
|
-
<NewPlacementForm />
|
|
32
|
-
</PrimaryPageTemplate>
|
|
33
|
-
)
|
|
34
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { initialsFromDisplayName } from "@/lib/initials-from-name"
|
|
5
|
-
import {
|
|
6
|
-
COMPLIANCE_STATUS_BADGE_CLASS,
|
|
7
|
-
COMPLIANCE_STATUS_ICON,
|
|
8
|
-
COMPLIANCE_STATUS_LABEL,
|
|
9
|
-
} from "@/lib/list-status-badges"
|
|
10
|
-
import type { ComplianceItem } from "@/lib/mock/compliance"
|
|
11
|
-
import { BoardCardTwoLineBlock } from "@/components/data-views/board-card-primitives"
|
|
12
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
13
|
-
import {
|
|
14
|
-
ListPageBoardCard,
|
|
15
|
-
ListPageBoardCardAvatar,
|
|
16
|
-
ListPageBoardCardBadgeRow,
|
|
17
|
-
ListPageBoardCardBody,
|
|
18
|
-
ListPageBoardCardHeader,
|
|
19
|
-
ListPageBoardCardTitleRow,
|
|
20
|
-
} from "@/components/data-views/list-page-board-card"
|
|
21
|
-
import {
|
|
22
|
-
ListPageBoardTemplate,
|
|
23
|
-
type ListPageBoardColumnDef,
|
|
24
|
-
} from "@/components/data-views/list-page-board-template"
|
|
25
|
-
|
|
26
|
-
const NEUTRAL_COUNT_BADGE = "bg-muted/90 text-foreground"
|
|
27
|
-
|
|
28
|
-
const STATUS_BOARD_COLUMNS: ListPageBoardColumnDef<ComplianceItem>[] = [
|
|
29
|
-
{
|
|
30
|
-
id: "compliant",
|
|
31
|
-
label: "Compliant",
|
|
32
|
-
description: "On track",
|
|
33
|
-
filter: r => r.status === "compliant",
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: "due_soon",
|
|
37
|
-
label: "Due soon",
|
|
38
|
-
description: "Within window",
|
|
39
|
-
filter: r => r.status === "due_soon",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: "overdue",
|
|
43
|
-
label: "Overdue",
|
|
44
|
-
description: "Action required",
|
|
45
|
-
filter: r => r.status === "overdue",
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: "pending",
|
|
49
|
-
label: "Pending",
|
|
50
|
-
description: "Not started",
|
|
51
|
-
filter: r => r.status === "pending",
|
|
52
|
-
},
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
function categoryBoardColumns(rows: ComplianceItem[]): {
|
|
56
|
-
columns: ListPageBoardColumnDef<ComplianceItem>[]
|
|
57
|
-
badgeMap: Record<string, string>
|
|
58
|
-
} {
|
|
59
|
-
const labels = [...new Set(rows.map(r => r.category))].sort((a, b) => a.localeCompare(b))
|
|
60
|
-
const columns: ListPageBoardColumnDef<ComplianceItem>[] = labels.map(label => ({
|
|
61
|
-
id: `category:${label}`,
|
|
62
|
-
label,
|
|
63
|
-
filter: (r: ComplianceItem) => r.category === label,
|
|
64
|
-
}))
|
|
65
|
-
const badgeMap = Object.fromEntries(labels.map(l => [`category:${l}`, NEUTRAL_COUNT_BADGE]))
|
|
66
|
-
return { columns, badgeMap }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function useComplianceBoardModel(rows: ComplianceItem[], groupByColumnKey: string) {
|
|
70
|
-
return React.useMemo(() => {
|
|
71
|
-
if (groupByColumnKey === "category") {
|
|
72
|
-
const { columns, badgeMap } = categoryBoardColumns(rows)
|
|
73
|
-
return { columns, badgeMap }
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
columns: STATUS_BOARD_COLUMNS,
|
|
77
|
-
badgeMap: COMPLIANCE_STATUS_BADGE_CLASS as Record<string, string>,
|
|
78
|
-
}
|
|
79
|
-
}, [rows, groupByColumnKey])
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function ComplianceBoardCard({
|
|
83
|
-
row,
|
|
84
|
-
onRowActivate,
|
|
85
|
-
}: {
|
|
86
|
-
row: ComplianceItem
|
|
87
|
-
onRowActivate?: (row: ComplianceItem) => void
|
|
88
|
-
}) {
|
|
89
|
-
const ownerInitials = initialsFromDisplayName(row.owner)
|
|
90
|
-
return (
|
|
91
|
-
<ListPageBoardCard className="w-full" onClick={onRowActivate ? () => onRowActivate(row) : undefined}>
|
|
92
|
-
<ListPageBoardCardHeader>
|
|
93
|
-
<ListPageBoardCardTitleRow
|
|
94
|
-
title={row.title}
|
|
95
|
-
titleClassName="line-clamp-2"
|
|
96
|
-
trailing={<ListPageBoardCardAvatar initials={ownerInitials} />}
|
|
97
|
-
/>
|
|
98
|
-
<ListPageBoardCardBadgeRow>
|
|
99
|
-
<ListHubStatusBadge
|
|
100
|
-
surface="board"
|
|
101
|
-
label={COMPLIANCE_STATUS_LABEL[row.status]}
|
|
102
|
-
tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
|
|
103
|
-
icon={COMPLIANCE_STATUS_ICON[row.status]}
|
|
104
|
-
/>
|
|
105
|
-
</ListPageBoardCardBadgeRow>
|
|
106
|
-
<ListPageBoardCardBody>
|
|
107
|
-
<BoardCardTwoLineBlock iconClass="fa-tag" line1={row.category} line2={`Due ${row.dueDate}`} />
|
|
108
|
-
<BoardCardTwoLineBlock iconClass="fa-user" line1={row.owner} line2="Owner" />
|
|
109
|
-
</ListPageBoardCardBody>
|
|
110
|
-
</ListPageBoardCardHeader>
|
|
111
|
-
</ListPageBoardCard>
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const COMPLIANCE_BOARD_GROUP_OPTIONS = [
|
|
116
|
-
{ key: "status", label: "Status" },
|
|
117
|
-
{ key: "category", label: "Category" },
|
|
118
|
-
] as const
|
|
119
|
-
|
|
120
|
-
export function ComplianceBoardView({
|
|
121
|
-
rows,
|
|
122
|
-
groupByColumnKey,
|
|
123
|
-
onRowActivate,
|
|
124
|
-
}: {
|
|
125
|
-
rows: ComplianceItem[]
|
|
126
|
-
groupByColumnKey: string
|
|
127
|
-
onRowActivate?: (row: ComplianceItem) => void
|
|
128
|
-
}) {
|
|
129
|
-
const key = groupByColumnKey === "category" ? "category" : "status"
|
|
130
|
-
const { columns, badgeMap } = useComplianceBoardModel(rows, key)
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<ListPageBoardTemplate
|
|
134
|
-
columns={columns}
|
|
135
|
-
rows={rows}
|
|
136
|
-
getRowKey={r => r.id}
|
|
137
|
-
columnCountBadgeClassName={badgeMap}
|
|
138
|
-
emptyColumnLabel="No items"
|
|
139
|
-
renderCard={row => <ComplianceBoardCard row={row} onRowActivate={onRowActivate} />}
|
|
140
|
-
/>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Compliance list page — `ListPageTemplate` + `ComplianceTable`; view types from `@/components/data-views`.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as React from "react"
|
|
8
|
-
import {
|
|
9
|
-
ListPageTemplate,
|
|
10
|
-
type ViewTab,
|
|
11
|
-
dataListViewIcon,
|
|
12
|
-
type DataListViewType,
|
|
13
|
-
} from "@/components/data-views"
|
|
14
|
-
import { CompliancePageHeader } from "@/components/compliance-page-header"
|
|
15
|
-
import { ComplianceTable, type ComplianceTableHandle } from "@/components/compliance-table"
|
|
16
|
-
import { KeyMetrics } from "@/components/key-metrics"
|
|
17
|
-
import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
|
|
18
|
-
import { COMPLIANCE_ITEMS } from "@/lib/mock/compliance"
|
|
19
|
-
import { complianceKpiInsight, complianceKpiMetrics } from "@/lib/mock/compliance-kpi"
|
|
20
|
-
|
|
21
|
-
const DEFAULT_TABS: ViewTab[] = [
|
|
22
|
-
{
|
|
23
|
-
id: "obligations",
|
|
24
|
-
label: "Obligations",
|
|
25
|
-
viewType: "table",
|
|
26
|
-
icon: "fa-table",
|
|
27
|
-
filterId: "all",
|
|
28
|
-
},
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
export function ComplianceClient() {
|
|
32
|
-
const [exportOpen, setExportOpen] = React.useState(false)
|
|
33
|
-
const [showMetrics, setShowMetrics] = React.useState(true)
|
|
34
|
-
const tableRef = React.useRef<ComplianceTableHandle>(null)
|
|
35
|
-
const count = COMPLIANCE_ITEMS.length
|
|
36
|
-
|
|
37
|
-
const metrics = React.useMemo(() => complianceKpiMetrics(COMPLIANCE_ITEMS), [])
|
|
38
|
-
const insight = React.useMemo(() => complianceKpiInsight(COMPLIANCE_ITEMS), [])
|
|
39
|
-
|
|
40
|
-
useAskLeoPageContext(
|
|
41
|
-
React.useMemo(
|
|
42
|
-
() => ({
|
|
43
|
-
title: "Compliance",
|
|
44
|
-
description: `${count} obligations tracked on this hub.`,
|
|
45
|
-
suggestions: [
|
|
46
|
-
"What’s due this week?",
|
|
47
|
-
"Summarize open items by student",
|
|
48
|
-
],
|
|
49
|
-
}),
|
|
50
|
-
[count],
|
|
51
|
-
),
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<ListPageTemplate
|
|
56
|
-
defaultTabs={DEFAULT_TABS}
|
|
57
|
-
getTabCount={() => count}
|
|
58
|
-
tablePropertiesRef={tableRef}
|
|
59
|
-
header={
|
|
60
|
-
<CompliancePageHeader
|
|
61
|
-
itemCount={count}
|
|
62
|
-
onAddReview={() => {}}
|
|
63
|
-
onExport={() => setExportOpen(true)}
|
|
64
|
-
showMetrics={showMetrics}
|
|
65
|
-
onToggleMetrics={() => setShowMetrics(v => !v)}
|
|
66
|
-
/>
|
|
67
|
-
}
|
|
68
|
-
metrics={
|
|
69
|
-
<KeyMetrics
|
|
70
|
-
variant="flat"
|
|
71
|
-
metrics={metrics}
|
|
72
|
-
insight={insight}
|
|
73
|
-
showHeader={false}
|
|
74
|
-
metricsSingleRow
|
|
75
|
-
/>
|
|
76
|
-
}
|
|
77
|
-
showMetrics={showMetrics}
|
|
78
|
-
exportOpen={exportOpen}
|
|
79
|
-
onExportOpenChange={setExportOpen}
|
|
80
|
-
exportTotalRows={count}
|
|
81
|
-
renderContent={(tab, updateTab) => (
|
|
82
|
-
<ComplianceTable
|
|
83
|
-
key={tab.id}
|
|
84
|
-
ref={tableRef}
|
|
85
|
-
items={COMPLIANCE_ITEMS}
|
|
86
|
-
view={tab.viewType}
|
|
87
|
-
onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
|
|
88
|
-
/>
|
|
89
|
-
)}
|
|
90
|
-
/>
|
|
91
|
-
)
|
|
92
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
4
|
-
import { ListPageBoardCard } from "@/components/data-views/list-page-board-card"
|
|
5
|
-
import { DataRowList } from "@/components/data-views/data-row-list"
|
|
6
|
-
import {
|
|
7
|
-
COMPLIANCE_STATUS_BADGE_CLASS,
|
|
8
|
-
COMPLIANCE_STATUS_ICON,
|
|
9
|
-
COMPLIANCE_STATUS_LABEL,
|
|
10
|
-
} from "@/lib/list-status-badges"
|
|
11
|
-
import type { ComplianceItem } from "@/lib/mock/compliance"
|
|
12
|
-
|
|
13
|
-
export function ComplianceListView({
|
|
14
|
-
rows,
|
|
15
|
-
onRowActivate,
|
|
16
|
-
}: {
|
|
17
|
-
rows: ComplianceItem[]
|
|
18
|
-
onRowActivate?: (row: ComplianceItem) => void
|
|
19
|
-
}) {
|
|
20
|
-
return (
|
|
21
|
-
<DataRowList<ComplianceItem>
|
|
22
|
-
rows={rows}
|
|
23
|
-
getRowId={row => row.id}
|
|
24
|
-
emptyState="No compliance items match your filters."
|
|
25
|
-
ariaLabel="Compliance items"
|
|
26
|
-
renderRow={row => (
|
|
27
|
-
<ListPageBoardCard
|
|
28
|
-
layout="row"
|
|
29
|
-
rowContainerClassName="flex w-full flex-col gap-1 sm:flex-row sm:items-center sm:gap-4"
|
|
30
|
-
onClick={onRowActivate ? () => onRowActivate(row) : undefined}
|
|
31
|
-
rowEnd={
|
|
32
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
33
|
-
<ListHubStatusBadge
|
|
34
|
-
surface="board"
|
|
35
|
-
label={COMPLIANCE_STATUS_LABEL[row.status]}
|
|
36
|
-
tintClassName={COMPLIANCE_STATUS_BADGE_CLASS[row.status]}
|
|
37
|
-
icon={COMPLIANCE_STATUS_ICON[row.status]}
|
|
38
|
-
/>
|
|
39
|
-
<i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
|
|
40
|
-
</div>
|
|
41
|
-
}
|
|
42
|
-
>
|
|
43
|
-
<div className="space-y-0.5">
|
|
44
|
-
<p className="text-sm font-semibold text-foreground">{row.title}</p>
|
|
45
|
-
<p className="text-xs text-muted-foreground">
|
|
46
|
-
{row.category} · Due {row.dueDate}
|
|
47
|
-
</p>
|
|
48
|
-
<p className="text-xs text-muted-foreground">Owner: {row.owner}</p>
|
|
49
|
-
</div>
|
|
50
|
-
</ListPageBoardCard>
|
|
51
|
-
)}
|
|
52
|
-
/>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
import { Button } from "@/components/ui/button"
|
|
5
|
-
import { PageHeader } from "@/components/page-header"
|
|
6
|
-
import {
|
|
7
|
-
DropdownMenu,
|
|
8
|
-
DropdownMenuContent,
|
|
9
|
-
DropdownMenuItem,
|
|
10
|
-
DropdownMenuSeparator,
|
|
11
|
-
DropdownMenuTrigger,
|
|
12
|
-
} from "@/components/ui/dropdown-menu"
|
|
13
|
-
import { Tip } from "@/components/ui/tip"
|
|
14
|
-
|
|
15
|
-
export interface CompliancePageHeaderProps {
|
|
16
|
-
itemCount: number
|
|
17
|
-
onAddReview: () => void
|
|
18
|
-
onExport: () => void
|
|
19
|
-
showMetrics: boolean
|
|
20
|
-
onToggleMetrics: () => void
|
|
21
|
-
showTitleBlock?: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function CompliancePageHeader({
|
|
25
|
-
itemCount,
|
|
26
|
-
onAddReview,
|
|
27
|
-
onExport,
|
|
28
|
-
showMetrics,
|
|
29
|
-
onToggleMetrics,
|
|
30
|
-
showTitleBlock = true,
|
|
31
|
-
}: CompliancePageHeaderProps) {
|
|
32
|
-
const [moreOpen, setMoreOpen] = React.useState(false)
|
|
33
|
-
const countLine = `${itemCount} ${itemCount === 1 ? "item" : "items"} · Last updated now`
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<PageHeader
|
|
37
|
-
title="Compliance"
|
|
38
|
-
subtitle={countLine}
|
|
39
|
-
showTitleBlock={showTitleBlock}
|
|
40
|
-
actions={(
|
|
41
|
-
<div className="flex items-center gap-2" role="group" aria-label="Compliance actions">
|
|
42
|
-
<Tip side="bottom" label="Schedule a review (demo)">
|
|
43
|
-
<Button type="button" size="lg" onClick={onAddReview}>
|
|
44
|
-
<i className="fa-light fa-calendar-check" aria-hidden="true" />
|
|
45
|
-
New review
|
|
46
|
-
</Button>
|
|
47
|
-
</Tip>
|
|
48
|
-
<DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
|
|
49
|
-
<Tip side="bottom" label="More actions">
|
|
50
|
-
<DropdownMenuTrigger asChild>
|
|
51
|
-
<Button
|
|
52
|
-
type="button"
|
|
53
|
-
size="lg"
|
|
54
|
-
variant="outline"
|
|
55
|
-
className="aspect-square px-0"
|
|
56
|
-
aria-label="More actions"
|
|
57
|
-
>
|
|
58
|
-
<i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
|
|
59
|
-
</Button>
|
|
60
|
-
</DropdownMenuTrigger>
|
|
61
|
-
</Tip>
|
|
62
|
-
<DropdownMenuContent align="end">
|
|
63
|
-
<DropdownMenuItem
|
|
64
|
-
onSelect={() => {
|
|
65
|
-
window.setTimeout(() => onExport(), 0)
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
|
|
69
|
-
Export
|
|
70
|
-
</DropdownMenuItem>
|
|
71
|
-
<DropdownMenuSeparator />
|
|
72
|
-
<DropdownMenuItem
|
|
73
|
-
onSelect={() => {
|
|
74
|
-
window.setTimeout(() => onToggleMetrics(), 0)
|
|
75
|
-
}}
|
|
76
|
-
>
|
|
77
|
-
<i
|
|
78
|
-
className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`}
|
|
79
|
-
aria-hidden="true"
|
|
80
|
-
/>
|
|
81
|
-
{showMetrics ? "Hide metric section" : "Show metric section"}
|
|
82
|
-
</DropdownMenuItem>
|
|
83
|
-
</DropdownMenuContent>
|
|
84
|
-
</DropdownMenu>
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
/>
|
|
88
|
-
)
|
|
89
|
-
}
|