@exxatdesignux/ui 0.2.16 → 0.2.18
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 +26 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +149 -4
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/accessibility.md +142 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/coach-marks.md +169 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/references/data-table-pattern.md +382 -0
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +38 -0
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +56 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +19 -0
- package/consumer-extras/patterns/data-views-pattern.md +2 -0
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +57 -0
- package/consumer-extras/patterns/shell-surface-elevation-pattern.md +52 -0
- package/package.json +3 -3
- package/src/components/ui/banner.tsx +2 -0
- package/src/components/ui/chart.tsx +57 -2
- package/src/components/ui/sidebar.tsx +3 -2
- package/src/globals.css +65 -14
- package/src/theme.css +3 -3
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +1 -1
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +1 -1
- package/template/.cursor/rules/exxat-mono-ids.mdc +30 -0
- package/template/AGENTS.md +27 -17
- package/template/app/(app)/data-list/page.tsx +2 -2
- package/template/app/(app)/error.tsx +22 -6
- package/template/app/(app)/layout.tsx +13 -6
- package/template/app/(app)/question-bank/layout.tsx +18 -5
- package/template/app/(app)/question-bank/new/page.tsx +58 -0
- package/template/app/global-error.tsx +63 -0
- package/template/app/globals.css +151 -14
- package/template/app/layout.tsx +43 -5
- package/template/components/app-sidebar.tsx +68 -33
- package/template/components/ask-leo-sidebar.tsx +0 -2
- package/template/components/brand-color-picker.tsx +344 -0
- package/template/components/compliance-list-view.tsx +33 -51
- package/template/components/compliance-table.tsx +4 -0
- package/template/components/data-table/index.tsx +99 -91
- package/template/components/data-table/pagination.tsx +0 -1
- package/template/components/data-table/types.ts +4 -1
- package/template/components/data-table/use-table-state.ts +276 -100
- package/template/components/data-views/data-row-list.tsx +183 -0
- package/template/components/data-views/index.ts +7 -3
- package/template/components/data-views/os-folder-glyph.tsx +8 -0
- package/template/components/dev-chunk-load-recovery.tsx +41 -0
- package/template/components/export-drawer.tsx +1 -1
- package/template/components/exxat-product-logo.tsx +168 -317
- package/template/components/invite-collaborators-drawer.tsx +5 -3
- package/template/components/key-metrics.tsx +122 -62
- package/template/components/new-placement-form.tsx +4 -2
- package/template/components/new-question-composer.tsx +2208 -0
- package/template/components/page-breadcrumb-trail.tsx +131 -0
- package/template/components/page-header.tsx +2 -1
- package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx} +2 -2
- package/template/components/placement-detail.tsx +1 -1
- package/template/components/placements-board-view.tsx +1 -1
- package/template/components/{data-list-client.tsx → placements-client.tsx} +9 -7
- package/template/components/placements-list-view.tsx +19 -133
- package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx} +2 -2
- package/template/components/{data-list-table-cells.tsx → placements-table-cells.tsx} +1 -1
- package/template/components/placements-table-columns.tsx +2 -2
- package/template/components/{data-list-table.tsx → placements-table.tsx} +42 -66
- package/template/components/product-switcher.tsx +24 -7
- package/template/components/product-wordmark.tsx +282 -0
- package/template/components/question-bank-client.tsx +20 -2
- package/template/components/question-bank-hub-client.tsx +105 -115
- package/template/components/question-bank-list-view.tsx +30 -54
- package/template/components/question-bank-new-folder-sheet.tsx +1 -1
- package/template/components/question-bank-secondary-nav.tsx +0 -3
- package/template/components/question-bank-table.tsx +19 -6
- package/template/components/rotations-empty-state.tsx +3 -0
- package/template/components/secondary-panel.tsx +23 -3
- package/template/components/settings-appearance-card.tsx +584 -141
- package/template/components/sidebar-shell.tsx +2 -1
- package/template/components/site-header.tsx +36 -31
- package/template/components/sites-list-view.tsx +31 -36
- package/template/components/sites-table.tsx +4 -0
- package/template/components/table-properties/drawer-button.tsx +38 -20
- package/template/components/table-properties/drawer.tsx +17 -14
- package/template/components/team-client.tsx +1 -1
- package/template/components/team-list-view.tsx +34 -50
- package/template/components/team-table.tsx +8 -3
- package/template/components/templates/list-page.tsx +12 -9
- package/template/components/templates/nested-secondary-panel-shell.tsx +10 -4
- package/template/components/ui/dot-pattern.tsx +50 -26
- package/template/components/ui/leo-icon.tsx +23 -3
- package/template/contexts/product-context.tsx +70 -7
- package/template/contexts/system-banner-context.tsx +112 -4
- package/template/docs/data-views-pattern.md +2 -0
- package/template/docs/kpi-flat-band-pattern.md +57 -0
- package/template/docs/kpi-strip-max-four-pattern.md +1 -0
- package/template/docs/shell-surface-elevation-pattern.md +52 -0
- package/template/eslint.config.mjs +18 -0
- package/template/hooks/use-sidebar-reflow-zoom.ts +21 -11
- package/template/lib/chunk-load-error.ts +13 -0
- package/template/lib/conditional-rule-match.ts +87 -22
- package/template/lib/data-list-persistence.ts +57 -257
- package/template/lib/data-list-view.ts +6 -0
- package/template/lib/dev-log.test.ts +6 -5
- package/template/lib/exxat-palette.json +1462 -0
- package/template/lib/exxat-palette.ts +136 -0
- package/template/lib/list-page-table-properties.ts +1 -1
- package/template/lib/list-status-badges.ts +1 -1
- package/template/lib/mailto.ts +29 -0
- package/template/lib/placement-board-card-layout.ts +1 -1
- package/template/lib/product-brand.ts +268 -0
- package/template/lib/question-bank-authoring.ts +308 -0
- package/template/lib/question-bank-nav.ts +44 -0
- package/template/lib/raf-throttle.ts +45 -0
- package/template/lib/sidebar-state-cookie.ts +9 -0
- package/template/lib/table-state-lifecycle.ts +521 -0
- package/template/next.config.mjs +156 -0
- package/template/package.json +3 -3
- package/template/stores/app-store.ts +46 -1
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import Link from "next/link"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import {
|
|
8
|
+
Breadcrumb,
|
|
9
|
+
BreadcrumbItem,
|
|
10
|
+
BreadcrumbLink,
|
|
11
|
+
BreadcrumbList,
|
|
12
|
+
BreadcrumbPage,
|
|
13
|
+
BreadcrumbSeparator,
|
|
14
|
+
} from "@/components/ui/breadcrumb"
|
|
15
|
+
|
|
16
|
+
export interface PageBreadcrumbTrailItem {
|
|
17
|
+
label: string
|
|
18
|
+
href?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PageBreadcrumbBackProps {
|
|
22
|
+
/** Destination label (e.g. "Question hub") — shown after the back icon. */
|
|
23
|
+
label: string
|
|
24
|
+
href: string
|
|
25
|
+
className?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PageBreadcrumbTrailProps {
|
|
29
|
+
/** Linkable ancestors (e.g. Question hub). */
|
|
30
|
+
items?: PageBreadcrumbTrailItem[]
|
|
31
|
+
/**
|
|
32
|
+
* Final segment in the trail. Omit when the current page is the `PageHeader`
|
|
33
|
+
* `<h1>` — use ancestors-only above the title (no duplicate label).
|
|
34
|
+
*/
|
|
35
|
+
currentPage?: string
|
|
36
|
+
/**
|
|
37
|
+
* `header` — SiteHeader: ancestors + `currentPage` on one line.
|
|
38
|
+
* `content` — ancestors only, above `PageHeader` title.
|
|
39
|
+
*/
|
|
40
|
+
variant?: "header" | "content"
|
|
41
|
+
className?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Single-step back nav — back icon + parent destination (no chevron trail).
|
|
46
|
+
* Use in `SiteHeader` for focused child routes (composer, wizard) where the
|
|
47
|
+
* page `<h1>` is the current title.
|
|
48
|
+
*/
|
|
49
|
+
export function PageBreadcrumbBack({ label, href, className }: PageBreadcrumbBackProps) {
|
|
50
|
+
return (
|
|
51
|
+
<Breadcrumb className={cn("min-w-0", className)}>
|
|
52
|
+
<BreadcrumbList className="gap-1.5 font-sans tracking-normal">
|
|
53
|
+
<BreadcrumbItem className="min-w-0">
|
|
54
|
+
<BreadcrumbLink asChild>
|
|
55
|
+
<Link
|
|
56
|
+
href={href}
|
|
57
|
+
className="group inline-flex min-w-0 max-w-full items-center gap-1.5 font-sans text-sm text-muted-foreground hover:text-interactive-hover-foreground"
|
|
58
|
+
aria-label={`Back to ${label}`}
|
|
59
|
+
>
|
|
60
|
+
<i
|
|
61
|
+
className="fa-light fa-arrow-left shrink-0 text-xs transition-transform group-hover:-translate-x-0.5"
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
/>
|
|
64
|
+
<span className="truncate">{label}</span>
|
|
65
|
+
</Link>
|
|
66
|
+
</BreadcrumbLink>
|
|
67
|
+
</BreadcrumbItem>
|
|
68
|
+
</BreadcrumbList>
|
|
69
|
+
</Breadcrumb>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Product breadcrumb trail — one component for SiteHeader and in-page shells.
|
|
75
|
+
* Uses shadcn `Breadcrumb` primitives with Exxat site-header typography.
|
|
76
|
+
*
|
|
77
|
+
* For back-icon + parent label only, use {@link PageBreadcrumbBack}.
|
|
78
|
+
*/
|
|
79
|
+
export function PageBreadcrumbTrail({
|
|
80
|
+
items = [],
|
|
81
|
+
currentPage,
|
|
82
|
+
variant = "content",
|
|
83
|
+
className,
|
|
84
|
+
}: PageBreadcrumbTrailProps) {
|
|
85
|
+
const isHeader = variant === "header"
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Breadcrumb
|
|
89
|
+
className={cn("min-w-0", className)}
|
|
90
|
+
aria-label={isHeader ? undefined : "Breadcrumb"}
|
|
91
|
+
>
|
|
92
|
+
<BreadcrumbList
|
|
93
|
+
className={cn(
|
|
94
|
+
"gap-1.5 font-sans tracking-normal",
|
|
95
|
+
isHeader && "flex-nowrap overflow-hidden",
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{items.map((crumb, i) => (
|
|
99
|
+
<React.Fragment key={`${crumb.label}-${i}`}>
|
|
100
|
+
<BreadcrumbItem className="shrink-0">
|
|
101
|
+
{crumb.href ? (
|
|
102
|
+
<BreadcrumbLink asChild>
|
|
103
|
+
<Link
|
|
104
|
+
href={crumb.href}
|
|
105
|
+
className="font-sans text-sm text-muted-foreground"
|
|
106
|
+
>
|
|
107
|
+
{crumb.label}
|
|
108
|
+
</Link>
|
|
109
|
+
</BreadcrumbLink>
|
|
110
|
+
) : (
|
|
111
|
+
<span className="font-sans text-sm text-muted-foreground">
|
|
112
|
+
{crumb.label}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
115
|
+
</BreadcrumbItem>
|
|
116
|
+
{(currentPage != null || i < items.length - 1) && (
|
|
117
|
+
<BreadcrumbSeparator className="text-muted-foreground/50 [&>i]:text-xs" />
|
|
118
|
+
)}
|
|
119
|
+
</React.Fragment>
|
|
120
|
+
))}
|
|
121
|
+
{currentPage != null ? (
|
|
122
|
+
<BreadcrumbItem className="min-w-0">
|
|
123
|
+
<BreadcrumbPage className="truncate font-sans text-sm font-medium">
|
|
124
|
+
{currentPage}
|
|
125
|
+
</BreadcrumbPage>
|
|
126
|
+
</BreadcrumbItem>
|
|
127
|
+
) : null}
|
|
128
|
+
</BreadcrumbList>
|
|
129
|
+
</Breadcrumb>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -47,7 +47,7 @@ export interface PageHeaderProps {
|
|
|
47
47
|
/** Primary page title — rendered as <h1> in Ivy Presto serif */
|
|
48
48
|
title: string
|
|
49
49
|
/** Short descriptor or date shown below the title (and below `accessInfo` when set) */
|
|
50
|
-
subtitle?:
|
|
50
|
+
subtitle?: React.ReactNode
|
|
51
51
|
/** Layout preset — `collaboration` enables access line + face row ahead of `actions`. */
|
|
52
52
|
variant?: PageHeaderVariant
|
|
53
53
|
/**
|
|
@@ -187,6 +187,7 @@ export function PageHeader({
|
|
|
187
187
|
<h1
|
|
188
188
|
className="text-2xl font-semibold tracking-tight leading-tight text-foreground"
|
|
189
189
|
style={{ fontFamily: "var(--font-heading)" }}
|
|
190
|
+
suppressHydrationWarning
|
|
190
191
|
>
|
|
191
192
|
{title}
|
|
192
193
|
</h1>
|
package/template/components/{data-views/placement-board-card.tsx → placement-board-card.tsx}
RENAMED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as React from "react"
|
|
8
8
|
import { cn } from "@/lib/utils"
|
|
9
9
|
import type { Placement } from "@/lib/mock/placements"
|
|
10
|
-
import { StatusBadge } from "@/components/
|
|
10
|
+
import { StatusBadge } from "@/components/placements-table-cells"
|
|
11
11
|
import { AvatarInitials } from "@/components/ui/avatar"
|
|
12
12
|
import { Badge } from "@/components/ui/badge"
|
|
13
13
|
import {
|
|
@@ -173,7 +173,7 @@ export function BoardPlacementCard({
|
|
|
173
173
|
onOpen: (id: number) => void
|
|
174
174
|
}) {
|
|
175
175
|
const lc = lineClampClass(lineCount)
|
|
176
|
-
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
176
|
+
const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
|
|
177
177
|
|
|
178
178
|
const visibleCols = boardColumns.filter(c => !hiddenColKeys.has(c.key))
|
|
179
179
|
const showStudent = visibleCols.some(c => c.key === "student")
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "@/components/ui/dropdown-menu"
|
|
18
18
|
import { cn } from "@/lib/utils"
|
|
19
19
|
import type { Placement } from "@/lib/mock/placements"
|
|
20
|
-
import { StatusBadge as PlacementStatusBadge } from "@/components/
|
|
20
|
+
import { StatusBadge as PlacementStatusBadge } from "@/components/placements-table-cells"
|
|
21
21
|
import { placementReadinessBadgeClass } from "@/lib/list-status-badges"
|
|
22
22
|
|
|
23
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -27,7 +27,7 @@ import { type BoardCardLifecycleTabId } from "@/lib/placement-board-card-layout"
|
|
|
27
27
|
import type { ConditionalRule } from "@/components/table-properties/types"
|
|
28
28
|
import type { ColumnDef } from "@/components/data-table/types"
|
|
29
29
|
import { Badge } from "@/components/ui/badge"
|
|
30
|
-
import { BoardPlacementCard } from "@/components/
|
|
30
|
+
import { BoardPlacementCard } from "@/components/placement-board-card"
|
|
31
31
|
import { BoardNewCardPlaceholder } from "@/components/data-views/board-card-primitives"
|
|
32
32
|
|
|
33
33
|
const PHASE_COLUMNS: { phase: PlacementPhase; label: string; description: string }[] = [
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* PlacementsClient — placements hub composition on the reusable
|
|
5
|
+
* `ListPageTemplate`. Owns the per-page persisted layout (tabs, display
|
|
6
|
+
* options, show-metrics toggle) and mounts `PlacementsTable` per tab.
|
|
5
7
|
*
|
|
6
8
|
* Uses centralized exports from `@/components/data-views`.
|
|
7
9
|
*/
|
|
@@ -12,8 +14,8 @@ import { useSidebar } from "@/components/ui/sidebar"
|
|
|
12
14
|
import {
|
|
13
15
|
ListPageTemplate,
|
|
14
16
|
type ViewTab,
|
|
15
|
-
|
|
16
|
-
type
|
|
17
|
+
PlacementsTable,
|
|
18
|
+
type PlacementsTableHandle,
|
|
17
19
|
type PlacementLifecycleTabId,
|
|
18
20
|
type DataListViewType,
|
|
19
21
|
dataListViewIcon,
|
|
@@ -127,7 +129,7 @@ const LIFECYCLE_OPTIONS = [
|
|
|
127
129
|
// Component
|
|
128
130
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
131
|
|
|
130
|
-
export function
|
|
132
|
+
export function PlacementsClient() {
|
|
131
133
|
const router = useRouter()
|
|
132
134
|
const { setOpen } = useSidebar()
|
|
133
135
|
const [showMetrics, setShowMetrics] = React.useState(true)
|
|
@@ -135,7 +137,7 @@ export function DataListClient() {
|
|
|
135
137
|
const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
|
|
136
138
|
const [tabs, setTabs] = React.useState<ViewTab[]>(DEFAULT_TABS)
|
|
137
139
|
const [activeTabId, setActiveTabId] = React.useState<string>(DEFAULT_TABS[0]?.id ?? "")
|
|
138
|
-
const tableRef = React.useRef<
|
|
140
|
+
const tableRef = React.useRef<PlacementsTableHandle>(null)
|
|
139
141
|
|
|
140
142
|
const viewsTour = useCoachMark({
|
|
141
143
|
flowId: "data-list-views-tour",
|
|
@@ -168,7 +170,7 @@ export function DataListClient() {
|
|
|
168
170
|
React.useLayoutEffect(() => {
|
|
169
171
|
const p = loadPageFromStorage()
|
|
170
172
|
if (!p) return
|
|
171
|
-
setDisplayOptions(
|
|
173
|
+
setDisplayOptions({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...p.displayOptions })
|
|
172
174
|
setShowMetrics(p.showMetrics)
|
|
173
175
|
setTabs(p.tabs)
|
|
174
176
|
const nextActive = p.tabs.some(t => t.id === p.activeTabId) ? p.activeTabId : (p.tabs[0]?.id ?? "")
|
|
@@ -226,7 +228,7 @@ export function DataListClient() {
|
|
|
226
228
|
renderContent={(tab, updateTab) => {
|
|
227
229
|
const phase = segmentFilterToPhase(tab.filterId)
|
|
228
230
|
return (
|
|
229
|
-
<
|
|
231
|
+
<PlacementsTable
|
|
230
232
|
key={tab.id}
|
|
231
233
|
ref={tableRef}
|
|
232
234
|
view={tab.viewType}
|
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* PlacementsListView — full-width row layout for the data list (vs table grid / board columns).
|
|
5
5
|
* Shares column visibility + lifecycle rules with Table Properties via the same board column model.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
|
+
* Shell (empty state + virtualization above 80 rows) comes from the generic
|
|
8
|
+
* `DataRowList` primitive in `components/data-views/`. This file owns only
|
|
9
|
+
* the placement-specific row body (column-driven field visibility).
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import * as React from "react"
|
|
10
13
|
import { useRouter } from "next/navigation"
|
|
11
|
-
import { useWindowVirtualizer } from "@tanstack/react-virtual"
|
|
12
|
-
import { cn } from "@/lib/utils"
|
|
13
14
|
import type { Placement } from "@/lib/mock/placements"
|
|
14
|
-
import { StatusBadge } from "@/components/
|
|
15
|
+
import { StatusBadge } from "@/components/placements-table-cells"
|
|
15
16
|
import { Badge } from "@/components/ui/badge"
|
|
16
17
|
import { ListPageBoardCard, ListPageBoardCardAvatar } from "@/components/data-views/list-page-board-card"
|
|
18
|
+
import { DataRowList } from "@/components/data-views/data-row-list"
|
|
17
19
|
import {
|
|
18
20
|
type BoardCardLifecycleTabId,
|
|
19
21
|
isBoardFieldActive,
|
|
@@ -23,7 +25,7 @@ import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
|
|
|
23
25
|
import type { ConditionalRule } from "@/components/table-properties/types"
|
|
24
26
|
import type { ColumnDef } from "@/components/data-table/types"
|
|
25
27
|
|
|
26
|
-
/** Above this count,
|
|
28
|
+
/** Above this count, `DataRowList` virtualizes against the window scroll. */
|
|
27
29
|
const VIRTUAL_ROWS_THRESHOLD = 80
|
|
28
30
|
/** Initial row height guess (px); `measureElement` refines for variable content. */
|
|
29
31
|
const ESTIMATE_ROW_PX = 100
|
|
@@ -60,7 +62,7 @@ function PlacementListRowContent({
|
|
|
60
62
|
conditionalRules: ConditionalRule[] | undefined
|
|
61
63
|
onOpen: (id: number) => void
|
|
62
64
|
}) {
|
|
63
|
-
const ruleBg = getConditionalRowBackground(row, conditionalRules)
|
|
65
|
+
const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
|
|
64
66
|
const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
|
|
65
67
|
const showStatus = isBoardFieldActive("status", tab, hiddenColKeys, boardColumns)
|
|
66
68
|
const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
|
|
@@ -126,106 +128,6 @@ function PlacementListRowContent({
|
|
|
126
128
|
)
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
function PlacementListRow({
|
|
130
|
-
row,
|
|
131
|
-
tab,
|
|
132
|
-
hiddenColKeys,
|
|
133
|
-
boardColumns,
|
|
134
|
-
conditionalRules,
|
|
135
|
-
onOpen,
|
|
136
|
-
}: {
|
|
137
|
-
row: Placement
|
|
138
|
-
tab: BoardCardLifecycleTabId
|
|
139
|
-
hiddenColKeys: Set<string>
|
|
140
|
-
boardColumns: ColumnDef<Placement>[]
|
|
141
|
-
conditionalRules: ConditionalRule[] | undefined
|
|
142
|
-
onOpen: (id: number) => void
|
|
143
|
-
}) {
|
|
144
|
-
return (
|
|
145
|
-
<li>
|
|
146
|
-
<PlacementListRowContent
|
|
147
|
-
row={row}
|
|
148
|
-
tab={tab}
|
|
149
|
-
hiddenColKeys={hiddenColKeys}
|
|
150
|
-
boardColumns={boardColumns}
|
|
151
|
-
conditionalRules={conditionalRules}
|
|
152
|
-
onOpen={onOpen}
|
|
153
|
-
/>
|
|
154
|
-
</li>
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function PlacementsListViewVirtualized({
|
|
159
|
-
rows,
|
|
160
|
-
lifecycleTabId,
|
|
161
|
-
hiddenColKeys,
|
|
162
|
-
boardColumns,
|
|
163
|
-
conditionalRules,
|
|
164
|
-
onOpen,
|
|
165
|
-
}: {
|
|
166
|
-
rows: Placement[]
|
|
167
|
-
lifecycleTabId: BoardCardLifecycleTabId
|
|
168
|
-
hiddenColKeys: Set<string>
|
|
169
|
-
boardColumns: ColumnDef<Placement>[]
|
|
170
|
-
conditionalRules: ConditionalRule[] | undefined
|
|
171
|
-
onOpen: (id: number) => void
|
|
172
|
-
}) {
|
|
173
|
-
const anchorRef = React.useRef<HTMLDivElement>(null)
|
|
174
|
-
const [scrollMargin, setScrollMargin] = React.useState(0)
|
|
175
|
-
|
|
176
|
-
const updateScrollMargin = React.useCallback(() => {
|
|
177
|
-
const el = anchorRef.current
|
|
178
|
-
if (!el) return
|
|
179
|
-
setScrollMargin(el.getBoundingClientRect().top + window.scrollY)
|
|
180
|
-
}, [])
|
|
181
|
-
|
|
182
|
-
React.useLayoutEffect(() => {
|
|
183
|
-
updateScrollMargin()
|
|
184
|
-
window.addEventListener("resize", updateScrollMargin)
|
|
185
|
-
return () => window.removeEventListener("resize", updateScrollMargin)
|
|
186
|
-
}, [updateScrollMargin, rows.length, lifecycleTabId])
|
|
187
|
-
|
|
188
|
-
const virtualizer = useWindowVirtualizer({
|
|
189
|
-
count: rows.length,
|
|
190
|
-
estimateSize: () => ESTIMATE_ROW_PX,
|
|
191
|
-
overscan: 8,
|
|
192
|
-
scrollMargin,
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
return (
|
|
196
|
-
<div ref={anchorRef} className="px-4 pb-8 pt-2 lg:px-6">
|
|
197
|
-
<ul
|
|
198
|
-
role="list"
|
|
199
|
-
className="relative m-0 w-full list-none p-0"
|
|
200
|
-
style={{ height: virtualizer.getTotalSize() }}
|
|
201
|
-
>
|
|
202
|
-
{virtualizer.getVirtualItems().map(vr => {
|
|
203
|
-
const row = rows[vr.index]
|
|
204
|
-
if (!row) return null
|
|
205
|
-
return (
|
|
206
|
-
<li
|
|
207
|
-
key={vr.key}
|
|
208
|
-
data-index={vr.index}
|
|
209
|
-
ref={virtualizer.measureElement}
|
|
210
|
-
className="absolute left-0 top-0 w-full pb-2"
|
|
211
|
-
style={{ transform: `translateY(${vr.start}px)` }}
|
|
212
|
-
>
|
|
213
|
-
<PlacementListRowContent
|
|
214
|
-
row={row}
|
|
215
|
-
tab={lifecycleTabId}
|
|
216
|
-
hiddenColKeys={hiddenColKeys}
|
|
217
|
-
boardColumns={boardColumns}
|
|
218
|
-
conditionalRules={conditionalRules}
|
|
219
|
-
onOpen={onOpen}
|
|
220
|
-
/>
|
|
221
|
-
</li>
|
|
222
|
-
)
|
|
223
|
-
})}
|
|
224
|
-
</ul>
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
131
|
export interface PlacementsListViewProps {
|
|
230
132
|
rows: Placement[]
|
|
231
133
|
lifecycleTabId: BoardCardLifecycleTabId
|
|
@@ -246,32 +148,16 @@ export function PlacementsListView({
|
|
|
246
148
|
const router = useRouter()
|
|
247
149
|
const onOpen = React.useCallback((id: number) => router.push(`/data-list/${id}`), [router])
|
|
248
150
|
|
|
249
|
-
if (rows.length === 0) {
|
|
250
|
-
return (
|
|
251
|
-
<div className="px-4 py-16 text-center lg:px-6">
|
|
252
|
-
<p className="text-sm text-muted-foreground">{emptyCopy}</p>
|
|
253
|
-
</div>
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (rows.length >= VIRTUAL_ROWS_THRESHOLD) {
|
|
258
|
-
return (
|
|
259
|
-
<PlacementsListViewVirtualized
|
|
260
|
-
rows={rows}
|
|
261
|
-
lifecycleTabId={lifecycleTabId}
|
|
262
|
-
hiddenColKeys={hiddenColKeys}
|
|
263
|
-
boardColumns={boardColumns}
|
|
264
|
-
conditionalRules={conditionalRules}
|
|
265
|
-
onOpen={onOpen}
|
|
266
|
-
/>
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
151
|
return (
|
|
271
|
-
<
|
|
272
|
-
{rows
|
|
273
|
-
|
|
274
|
-
|
|
152
|
+
<DataRowList<Placement>
|
|
153
|
+
rows={rows}
|
|
154
|
+
getRowId={row => row.id}
|
|
155
|
+
emptyState={emptyCopy}
|
|
156
|
+
ariaLabel="Placements"
|
|
157
|
+
virtualizeThreshold={VIRTUAL_ROWS_THRESHOLD}
|
|
158
|
+
estimatedRowHeight={ESTIMATE_ROW_PX}
|
|
159
|
+
renderRow={row => (
|
|
160
|
+
<PlacementListRowContent
|
|
275
161
|
row={row}
|
|
276
162
|
tab={lifecycleTabId}
|
|
277
163
|
hiddenColKeys={hiddenColKeys}
|
|
@@ -279,7 +165,7 @@ export function PlacementsListView({
|
|
|
279
165
|
conditionalRules={conditionalRules}
|
|
280
166
|
onOpen={onOpen}
|
|
281
167
|
/>
|
|
282
|
-
)
|
|
283
|
-
|
|
168
|
+
)}
|
|
169
|
+
/>
|
|
284
170
|
)
|
|
285
171
|
}
|
package/template/components/{data-list-table-cells.test.tsx → placements-table-cells.test.tsx}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { render, screen } from "@testing-library/react"
|
|
3
3
|
|
|
4
|
-
import { HireBadge, ReadinessBadge, StatusBadge } from "./
|
|
4
|
+
import { HireBadge, ReadinessBadge, StatusBadge } from "./placements-table-cells"
|
|
5
5
|
|
|
6
|
-
describe("
|
|
6
|
+
describe("placements-table-cells", () => {
|
|
7
7
|
it("renders StatusBadge label for confirmed", () => {
|
|
8
8
|
render(<StatusBadge status="confirmed" />)
|
|
9
9
|
expect(screen.getByText("Confirmed")).toBeInTheDocument()
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Placements lifecycle columns, empty states, and Properties drawer labels.
|
|
5
|
-
* Owned by the placements
|
|
5
|
+
* Owned by the placements feature (`PlacementsClient` / `/data-list`); consumed by `PlacementsTable`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Badge } from "@/components/ui/badge"
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
RowActions,
|
|
17
17
|
StatusBadge,
|
|
18
18
|
WeeksProgressCell,
|
|
19
|
-
} from "@/components/
|
|
19
|
+
} from "@/components/placements-table-cells"
|
|
20
20
|
import { uniquePlacementFieldOptions, type Placement } from "@/lib/mock/placements"
|
|
21
21
|
import { formatDateUS } from "@/lib/date-filter"
|
|
22
22
|
import type { PlacementLifecycleTabId } from "@/lib/placement-lifecycle"
|