@exxatdesignux/ui 0.3.0 → 0.4.1
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 +701 -6
- package/README.md +138 -0
- package/bin/init.mjs +134 -31
- package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
- package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
- package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
- package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
- package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
- package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
- package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
- package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
- package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
- package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
- package/consumer-extras/handbook/HANDBOOK.md +2 -0
- package/consumer-extras/handbook/glossary.md +2 -1
- package/consumer-extras/handbook/reference-implementations.md +31 -4
- package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
- package/consumer-extras/patterns/data-views-pattern.md +18 -16
- package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
- package/dist/components/data-table/index.js +2 -2
- package/dist/components/data-table/index.js.map +1 -1
- package/dist/components/data-table/pagination.js +3 -3
- package/dist/components/data-table/pagination.js.map +1 -1
- package/dist/components/data-table/use-table-state.d.ts +1 -1
- package/dist/components/data-table/use-table-state.js.map +1 -1
- package/dist/components/data-views/data-row-list.js.map +1 -1
- package/dist/components/data-views/finder-panel-view.d.ts +1 -1
- package/dist/components/data-views/finder-panel-view.js.map +1 -1
- package/dist/components/data-views/hub-table.d.ts +9 -3
- package/dist/components/data-views/hub-table.js +262 -40
- package/dist/components/data-views/hub-table.js.map +1 -1
- package/dist/components/data-views/index.js +262 -40
- package/dist/components/data-views/index.js.map +1 -1
- package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
- package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
- package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
- package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
- package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
- package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
- package/dist/components/data-views/os-folder-glyph.js.map +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/key-metrics.js.map +1 -1
- package/dist/index.js +136 -39
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/data-table/index.tsx +2 -2
- package/src/components/data-table/pagination.tsx +5 -1
- package/src/components/data-table/use-table-state.ts +1 -1
- package/src/components/data-views/data-row-list.tsx +1 -1
- package/src/components/data-views/finder-panel-view.tsx +2 -2
- package/src/components/data-views/hub-table.tsx +149 -41
- package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
- package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
- package/src/components/data-views/os-folder-glyph.tsx +1 -1
- package/src/components/ui/key-metrics.tsx +1 -1
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
- package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
- package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
- package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
- package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
- package/template/AGENTS.md +43 -37
- package/template/app/(app)/columns/page.tsx +11 -0
- package/template/app/(app)/library/all/page.tsx +11 -0
- package/template/app/(app)/library/find/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
- package/template/app/(app)/library/list/page.tsx +12 -0
- package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
- package/template/app/(app)/library/page.tsx +11 -0
- package/template/app/(app)/tokens-themes/page.tsx +11 -0
- package/template/components/ask-leo-composer.tsx +2 -2
- package/template/components/columns-client.tsx +158 -0
- package/template/components/columns-showcase.tsx +541 -0
- package/template/components/data-views/index.ts +32 -6
- package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
- package/template/components/data-views/table-cells.tsx +673 -0
- package/template/components/folder-details-shell.tsx +11 -11
- package/template/components/hub-tree-panel-view.tsx +24 -24
- package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
- package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
- package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
- package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
- package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
- package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
- package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
- package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
- package/template/components/library-panel-activator.tsx +8 -0
- package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
- package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
- package/template/components/list-hub-status-badge.tsx +2 -2
- package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
- package/template/components/sidebar/app-sidebar.tsx +61 -5
- package/template/components/sidebar/secondary-panel.tsx +109 -56
- package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
- package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
- package/template/components/table-properties/types.ts +1 -1
- package/template/components/templates/discovery-hub-template.tsx +1 -1
- package/template/components/templates/new-focus-template.tsx +2 -2
- package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
- package/template/components/tokens-secondary-nav.tsx +192 -0
- package/template/components/tokens-themes-client.tsx +476 -0
- package/template/components/tokens-themes-section.tsx +386 -0
- package/template/docs/HANDBOOK.md +187 -0
- package/template/docs/blueprints/README.md +1 -1
- package/template/docs/blueprints/board-card.md +1 -1
- package/template/docs/blueprints/data-table.md +2 -2
- package/template/docs/blueprints/list-page-template.md +3 -3
- package/template/docs/blueprints/page-header.md +4 -4
- package/template/docs/collaboration-access-pattern.md +7 -7
- package/template/docs/component-selection-guide.md +1 -1
- package/template/docs/data-views-pattern.md +18 -16
- package/template/docs/glossary.md +58 -0
- package/template/docs/kpi-flat-band-pattern.md +3 -3
- package/template/docs/kpi-trend-pattern.md +18 -3
- package/template/docs/large-dataset-strategy.md +155 -0
- package/template/docs/library-hub-header-pattern.md +25 -0
- package/template/docs/migrations/_template.md +1 -1
- package/template/docs/reference-implementations.md +151 -0
- package/template/docs/token-taxonomy.md +1 -1
- package/template/docs/voice-and-tone.md +262 -0
- package/template/eslint.config.mjs +9 -39
- package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
- package/template/lib/ask-leo-route-context.ts +6 -18
- package/template/lib/coach-mark-registry.ts +0 -16
- package/template/lib/command-menu-config.ts +5 -12
- package/template/lib/command-menu-search-data.ts +8 -39
- package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
- package/template/lib/library-dedicated-search.ts +19 -0
- package/template/lib/library-hub-search.ts +90 -0
- package/template/lib/library-nav.ts +477 -0
- package/template/lib/library-recent-searches.ts +22 -0
- package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
- package/template/lib/list-status-badges.ts +16 -104
- package/template/lib/mock/dashboard.ts +1 -1
- package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
- package/template/lib/mock/library-header-collaborators.ts +54 -0
- package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
- package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
- package/template/lib/mock/library.ts +249 -0
- package/template/lib/mock/navigation.tsx +32 -26
- package/template/lib/table-state-lifecycle.ts +1 -1
- package/template/next.config.mjs +7 -4
- package/template/package.json +0 -1
- package/tokens/hooks-index.json +2874 -0
- package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
- package/template/app/(app)/examples/page.tsx +0 -41
- package/template/app/(app)/question-bank/find/page.tsx +0 -12
- package/template/app/(app)/question-bank/library/page.tsx +0 -11
- package/template/app/(app)/question-bank/list/page.tsx +0 -12
- package/template/app/(app)/question-bank/page.tsx +0 -11
- package/template/components/compliance-board-view.tsx +0 -142
- package/template/components/compliance-client.tsx +0 -92
- package/template/components/compliance-page-header.tsx +0 -89
- package/template/components/compliance-table.tsx +0 -468
- 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 -942
- package/template/components/placement-board-card.tsx +0 -250
- package/template/components/placement-detail.tsx +0 -438
- package/template/components/placements-board-view.tsx +0 -397
- package/template/components/placements-client.tsx +0 -220
- package/template/components/placements-list-view.tsx +0 -124
- 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 -210
- package/template/components/placements-table.tsx +0 -934
- package/template/components/question-bank-panel-activator.tsx +0 -8
- package/template/components/rotations-empty-state.tsx +0 -50
- package/template/components/rotations-panel-activator.tsx +0 -8
- package/template/components/sites-board-view.tsx +0 -67
- package/template/components/sites-client.tsx +0 -154
- package/template/components/sites-table.tsx +0 -249
- package/template/components/team-board-view.tsx +0 -122
- package/template/components/team-client.tsx +0 -100
- package/template/components/team-page-header.tsx +0 -92
- package/template/components/team-table.tsx +0 -553
- package/template/docs/question-bank-hub-header-pattern.md +0 -25
- package/template/lib/compliance-supported-views.ts +0 -10
- 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 -176
- package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
- package/template/lib/mock/question-bank.ts +0 -249
- 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/question-bank-dedicated-search.ts +0 -19
- package/template/lib/question-bank-hub-search.ts +0 -90
- package/template/lib/question-bank-nav.ts +0 -477
- package/template/lib/question-bank-recent-searches.ts +0 -22
- package/template/lib/question-bank-supported-views.ts +0 -12
- package/template/lib/sites-supported-views.ts +0 -10
- package/template/lib/team-supported-views.ts +0 -10
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PlacementsBoardView — kanban-style board by placement phase (domain-specific columns).
|
|
5
|
-
* View chrome labels use `dataListViewLabel` from `@/lib/data-list-view` at the page level;
|
|
6
|
-
* this component focuses on placement phase grouping + shared card primitives.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as React from "react"
|
|
10
|
-
import { useRouter } from "next/navigation"
|
|
11
|
-
import { cn } from "@/lib/utils"
|
|
12
|
-
import type { Placement, PlacementPhase } from "@/lib/mock/placements"
|
|
13
|
-
import { Input } from "@/components/ui/input"
|
|
14
|
-
import { Tip } from "@/components/ui/tip"
|
|
15
|
-
import {
|
|
16
|
-
DropdownMenu,
|
|
17
|
-
DropdownMenuContent,
|
|
18
|
-
DropdownMenuItem,
|
|
19
|
-
DropdownMenuSeparator,
|
|
20
|
-
DropdownMenuSub,
|
|
21
|
-
DropdownMenuSubContent,
|
|
22
|
-
DropdownMenuSubTrigger,
|
|
23
|
-
DropdownMenuTrigger,
|
|
24
|
-
} from "@/components/ui/dropdown-menu"
|
|
25
|
-
import { DEFAULT_DATA_LIST_DISPLAY_OPTIONS, type BoardLineCount } from "@/lib/data-list-display-options"
|
|
26
|
-
import type { ConditionalRule } from "@/components/table-properties/types"
|
|
27
|
-
import type { ColumnDef } from "@/components/data-table/types"
|
|
28
|
-
import { Badge } from "@/components/ui/badge"
|
|
29
|
-
import { BoardPlacementCard } from "@/components/placement-board-card"
|
|
30
|
-
import { BoardNewCardPlaceholder } from "@/components/data-views/board-card-primitives"
|
|
31
|
-
|
|
32
|
-
const PHASE_COLUMNS: { phase: PlacementPhase; label: string; description: string }[] = [
|
|
33
|
-
{ phase: "upcoming", label: "Upcoming", description: "Starting soon" },
|
|
34
|
-
{ phase: "ongoing", label: "Ongoing", description: "In progress" },
|
|
35
|
-
{ phase: "completed", label: "Completed", description: "Finished" },
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
/** Substring match across visible card fields (per-phase quick search). */
|
|
39
|
-
function rowMatchesPhaseSearch(row: Placement, q: string): boolean {
|
|
40
|
-
if (!q.trim()) return true
|
|
41
|
-
const lower = q.toLowerCase()
|
|
42
|
-
const hay = [
|
|
43
|
-
row.student,
|
|
44
|
-
row.site,
|
|
45
|
-
row.specialization,
|
|
46
|
-
row.internship,
|
|
47
|
-
row.program,
|
|
48
|
-
row.status,
|
|
49
|
-
row.supervisor,
|
|
50
|
-
row.email,
|
|
51
|
-
row.start,
|
|
52
|
-
]
|
|
53
|
-
.map(v => String(v ?? "").toLowerCase())
|
|
54
|
-
.join(" ")
|
|
55
|
-
return hay.includes(lower)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface PlacementsBoardColumnMenu {
|
|
59
|
-
filterableColumns: { key: string; label: string }[]
|
|
60
|
-
sortableColumns: { key: string; label: string }[]
|
|
61
|
-
groupableColumns: { key: string; label: string }[]
|
|
62
|
-
groupBy: string | null
|
|
63
|
-
onAddFilter: (fieldKey: string) => void
|
|
64
|
-
onSortByField: (fieldKey: string, direction: "asc" | "desc") => void
|
|
65
|
-
onToggleGroupBy: (fieldKey: string) => void
|
|
66
|
-
onOpenProperties: () => void
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface BoardDisplaySettings {
|
|
70
|
-
lineCount: BoardLineCount
|
|
71
|
-
showColumnLabels: boolean
|
|
72
|
-
showColumnCounts: boolean
|
|
73
|
-
newCardAbove: boolean
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface PlacementsBoardViewProps {
|
|
77
|
-
placements: Placement[]
|
|
78
|
-
/** When set, each phase column header shows the same actions as a DataTable column header. */
|
|
79
|
-
boardColumnMenu?: PlacementsBoardColumnMenu
|
|
80
|
-
/** Board display options (Properties → view display). */
|
|
81
|
-
boardDisplay?: BoardDisplaySettings
|
|
82
|
-
/** Column visibility from table state — hidden columns omit matching card fields. */
|
|
83
|
-
hiddenColKeys?: Set<string>
|
|
84
|
-
/** Same conditional formatting as the table (row background when a rule matches). */
|
|
85
|
-
conditionalRules?: ConditionalRule[]
|
|
86
|
-
/** Visible data columns (table order) — drives dates and other fields on the card. */
|
|
87
|
-
boardColumns: ColumnDef<Placement>[]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function BoardPhaseColumnHeader({
|
|
91
|
-
label,
|
|
92
|
-
rawCount,
|
|
93
|
-
filteredCount,
|
|
94
|
-
searchValue,
|
|
95
|
-
onSearchChange,
|
|
96
|
-
menu,
|
|
97
|
-
showLabels,
|
|
98
|
-
showCounts,
|
|
99
|
-
}: {
|
|
100
|
-
label: string
|
|
101
|
-
rawCount: number
|
|
102
|
-
filteredCount: number
|
|
103
|
-
searchValue: string
|
|
104
|
-
onSearchChange: (value: string) => void
|
|
105
|
-
menu: PlacementsBoardColumnMenu
|
|
106
|
-
showLabels: boolean
|
|
107
|
-
showCounts: boolean
|
|
108
|
-
}) {
|
|
109
|
-
const searchActive = Boolean(searchValue.trim())
|
|
110
|
-
const countLabel =
|
|
111
|
-
searchActive && filteredCount !== rawCount
|
|
112
|
-
? `${filteredCount} of ${rawCount} records`
|
|
113
|
-
: `${filteredCount} ${filteredCount === 1 ? "record" : "records"}`
|
|
114
|
-
|
|
115
|
-
const showLeft = showLabels || showCounts
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div className="group/board-col border-b border-border px-3 py-2.5">
|
|
119
|
-
<div className="flex items-center justify-between gap-2">
|
|
120
|
-
{showLeft ? (
|
|
121
|
-
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
122
|
-
{showLabels ? (
|
|
123
|
-
<p className="min-w-0 truncate text-sm font-semibold text-foreground">{label}</p>
|
|
124
|
-
) : null}
|
|
125
|
-
{showCounts ? (
|
|
126
|
-
<div className="flex shrink-0 items-center gap-1.5">
|
|
127
|
-
<Badge
|
|
128
|
-
variant="outline"
|
|
129
|
-
className="inline-flex h-6 min-w-6 items-center justify-center border-0 bg-muted/70 px-2 text-xs font-semibold tabular-nums text-foreground"
|
|
130
|
-
aria-label={countLabel}
|
|
131
|
-
>
|
|
132
|
-
{filteredCount}
|
|
133
|
-
</Badge>
|
|
134
|
-
{searchActive && filteredCount !== rawCount ? (
|
|
135
|
-
<span className="text-xs font-medium tabular-nums text-muted-foreground" aria-hidden>
|
|
136
|
-
/ {rawCount}
|
|
137
|
-
</span>
|
|
138
|
-
) : null}
|
|
139
|
-
</div>
|
|
140
|
-
) : null}
|
|
141
|
-
</div>
|
|
142
|
-
) : (
|
|
143
|
-
<div className="min-w-0 flex-1" aria-hidden />
|
|
144
|
-
)}
|
|
145
|
-
<DropdownMenu>
|
|
146
|
-
<Tip label="Column options" side="top">
|
|
147
|
-
<DropdownMenuTrigger asChild>
|
|
148
|
-
<button
|
|
149
|
-
type="button"
|
|
150
|
-
aria-label={`${label} column options`}
|
|
151
|
-
onClick={e => e.stopPropagation()}
|
|
152
|
-
className={cn(
|
|
153
|
-
"opacity-0 group-hover/board-col:opacity-100 group-focus-within/board-col:opacity-100",
|
|
154
|
-
"inline-flex shrink-0 items-center justify-center size-7 rounded-md",
|
|
155
|
-
"text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover-row",
|
|
156
|
-
"transition-opacity focus-visible:opacity-100",
|
|
157
|
-
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
158
|
-
)}
|
|
159
|
-
>
|
|
160
|
-
<i className="fa-light fa-ellipsis-vertical text-xs" aria-hidden="true" />
|
|
161
|
-
</button>
|
|
162
|
-
</DropdownMenuTrigger>
|
|
163
|
-
</Tip>
|
|
164
|
-
<DropdownMenuContent align="end">
|
|
165
|
-
<div className="px-2 pt-2 pb-1">
|
|
166
|
-
<div className="relative">
|
|
167
|
-
<i
|
|
168
|
-
className="fa-light fa-magnifying-glass pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 text-xs text-muted-foreground"
|
|
169
|
-
aria-hidden="true"
|
|
170
|
-
/>
|
|
171
|
-
<Input
|
|
172
|
-
placeholder={`Search ${label}…`}
|
|
173
|
-
value={searchValue}
|
|
174
|
-
onChange={e => onSearchChange(e.target.value)}
|
|
175
|
-
onKeyDown={e => e.stopPropagation()}
|
|
176
|
-
className="h-7 ps-6 text-xs"
|
|
177
|
-
/>
|
|
178
|
-
{searchValue ? (
|
|
179
|
-
<button
|
|
180
|
-
type="button"
|
|
181
|
-
aria-label="Clear search"
|
|
182
|
-
onClick={() => onSearchChange("")}
|
|
183
|
-
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-interactive-hover-foreground"
|
|
184
|
-
>
|
|
185
|
-
<i className="fa-light fa-xmark text-xs" aria-hidden="true" />
|
|
186
|
-
</button>
|
|
187
|
-
) : null}
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
<DropdownMenuSeparator />
|
|
191
|
-
|
|
192
|
-
{menu.filterableColumns.length > 0 && (
|
|
193
|
-
<>
|
|
194
|
-
<DropdownMenuSub>
|
|
195
|
-
<DropdownMenuSubTrigger>
|
|
196
|
-
<i className="fa-light fa-filter" aria-hidden="true" />
|
|
197
|
-
Filter by field…
|
|
198
|
-
</DropdownMenuSubTrigger>
|
|
199
|
-
<DropdownMenuSubContent className="max-h-[min(280px,60vh)] overflow-y-auto">
|
|
200
|
-
{menu.filterableColumns.map(col => (
|
|
201
|
-
<DropdownMenuItem key={col.key} onClick={() => menu.onAddFilter(col.key)}>
|
|
202
|
-
{col.label}
|
|
203
|
-
</DropdownMenuItem>
|
|
204
|
-
))}
|
|
205
|
-
</DropdownMenuSubContent>
|
|
206
|
-
</DropdownMenuSub>
|
|
207
|
-
<DropdownMenuSeparator />
|
|
208
|
-
</>
|
|
209
|
-
)}
|
|
210
|
-
|
|
211
|
-
{menu.sortableColumns.length > 0 && (
|
|
212
|
-
<>
|
|
213
|
-
<DropdownMenuSub>
|
|
214
|
-
<DropdownMenuSubTrigger>
|
|
215
|
-
<i className="fa-light fa-arrow-up-arrow-down" aria-hidden="true" />
|
|
216
|
-
Sort by…
|
|
217
|
-
</DropdownMenuSubTrigger>
|
|
218
|
-
<DropdownMenuSubContent className="max-h-[min(320px,60vh)] overflow-y-auto">
|
|
219
|
-
{menu.sortableColumns.map(col => (
|
|
220
|
-
<React.Fragment key={col.key}>
|
|
221
|
-
<DropdownMenuItem onClick={() => menu.onSortByField(col.key, "asc")}>
|
|
222
|
-
<i className="fa-light fa-arrow-up-a-z" aria-hidden="true" />
|
|
223
|
-
{col.label} — ascending
|
|
224
|
-
</DropdownMenuItem>
|
|
225
|
-
<DropdownMenuItem onClick={() => menu.onSortByField(col.key, "desc")}>
|
|
226
|
-
<i className="fa-light fa-arrow-down-a-z" aria-hidden="true" />
|
|
227
|
-
{col.label} — descending
|
|
228
|
-
</DropdownMenuItem>
|
|
229
|
-
</React.Fragment>
|
|
230
|
-
))}
|
|
231
|
-
</DropdownMenuSubContent>
|
|
232
|
-
</DropdownMenuSub>
|
|
233
|
-
<DropdownMenuSeparator />
|
|
234
|
-
</>
|
|
235
|
-
)}
|
|
236
|
-
|
|
237
|
-
{menu.groupableColumns.length > 0 && (
|
|
238
|
-
<>
|
|
239
|
-
<DropdownMenuSub>
|
|
240
|
-
<DropdownMenuSubTrigger>
|
|
241
|
-
<i className="fa-light fa-layer-group" aria-hidden="true" />
|
|
242
|
-
Group by…
|
|
243
|
-
</DropdownMenuSubTrigger>
|
|
244
|
-
<DropdownMenuSubContent className="max-h-[min(280px,60vh)] overflow-y-auto">
|
|
245
|
-
{menu.groupableColumns.map(col => (
|
|
246
|
-
<DropdownMenuItem
|
|
247
|
-
key={col.key}
|
|
248
|
-
onClick={() => menu.onToggleGroupBy(col.key)}
|
|
249
|
-
>
|
|
250
|
-
{menu.groupBy === col.key ? (
|
|
251
|
-
<>
|
|
252
|
-
<i className="fa-light fa-check text-xs" aria-hidden="true" />
|
|
253
|
-
Grouped by {col.label}
|
|
254
|
-
</>
|
|
255
|
-
) : (
|
|
256
|
-
<>
|
|
257
|
-
<span className="inline-block w-3" aria-hidden />
|
|
258
|
-
Group by {col.label}
|
|
259
|
-
</>
|
|
260
|
-
)}
|
|
261
|
-
</DropdownMenuItem>
|
|
262
|
-
))}
|
|
263
|
-
</DropdownMenuSubContent>
|
|
264
|
-
</DropdownMenuSub>
|
|
265
|
-
<DropdownMenuSeparator />
|
|
266
|
-
</>
|
|
267
|
-
)}
|
|
268
|
-
|
|
269
|
-
<DropdownMenuItem onClick={menu.onOpenProperties}>
|
|
270
|
-
<i className="fa-light fa-palette" aria-hidden="true" />
|
|
271
|
-
Add conditional rule
|
|
272
|
-
</DropdownMenuItem>
|
|
273
|
-
</DropdownMenuContent>
|
|
274
|
-
</DropdownMenu>
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export function PlacementsBoardView({
|
|
281
|
-
placements,
|
|
282
|
-
boardColumnMenu,
|
|
283
|
-
boardDisplay: boardDisplayProp,
|
|
284
|
-
hiddenColKeys: hiddenColKeysProp,
|
|
285
|
-
conditionalRules,
|
|
286
|
-
boardColumns,
|
|
287
|
-
}: PlacementsBoardViewProps) {
|
|
288
|
-
const router = useRouter()
|
|
289
|
-
|
|
290
|
-
const bd: BoardDisplaySettings = {
|
|
291
|
-
lineCount: boardDisplayProp?.lineCount ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.boardLineCount,
|
|
292
|
-
showColumnLabels: boardDisplayProp?.showColumnLabels ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.showColumnLabels,
|
|
293
|
-
showColumnCounts: boardDisplayProp?.showColumnCounts ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.showBoardColumnCounts,
|
|
294
|
-
newCardAbove: boardDisplayProp?.newCardAbove ?? DEFAULT_DATA_LIST_DISPLAY_OPTIONS.boardNewCardAbove,
|
|
295
|
-
}
|
|
296
|
-
const hiddenColKeys = hiddenColKeysProp ?? new Set<string>()
|
|
297
|
-
|
|
298
|
-
const [phaseSearch, setPhaseSearch] = React.useState<Record<PlacementPhase, string>>({
|
|
299
|
-
upcoming: "",
|
|
300
|
-
ongoing: "",
|
|
301
|
-
completed: "",
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
const byPhase = React.useMemo(() => {
|
|
305
|
-
const map: Record<PlacementPhase, Placement[]> = {
|
|
306
|
-
upcoming: [],
|
|
307
|
-
ongoing: [],
|
|
308
|
-
completed: [],
|
|
309
|
-
}
|
|
310
|
-
for (const p of placements) {
|
|
311
|
-
map[p.placementPhase].push(p)
|
|
312
|
-
}
|
|
313
|
-
return map
|
|
314
|
-
}, [placements])
|
|
315
|
-
|
|
316
|
-
const cardsByPhase = React.useMemo(() => {
|
|
317
|
-
const out: Record<PlacementPhase, Placement[]> = {
|
|
318
|
-
upcoming: [],
|
|
319
|
-
ongoing: [],
|
|
320
|
-
completed: [],
|
|
321
|
-
}
|
|
322
|
-
for (const phase of PHASE_COLUMNS.map(c => c.phase)) {
|
|
323
|
-
const q = phaseSearch[phase]
|
|
324
|
-
out[phase] = byPhase[phase].filter(row => rowMatchesPhaseSearch(row, q))
|
|
325
|
-
}
|
|
326
|
-
return out
|
|
327
|
-
}, [byPhase, phaseSearch])
|
|
328
|
-
|
|
329
|
-
return (
|
|
330
|
-
<div className="px-4 pb-8 pt-2 lg:px-6">
|
|
331
|
-
<p className="text-xs text-muted-foreground mb-4">
|
|
332
|
-
Rows grouped by phase (same data as Table view and List view).
|
|
333
|
-
</p>
|
|
334
|
-
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 min-h-[min(480px,calc(100vh-14rem))]">
|
|
335
|
-
{PHASE_COLUMNS.map(col => {
|
|
336
|
-
const rawInPhase = byPhase[col.phase]
|
|
337
|
-
const cards = cardsByPhase[col.phase]
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<div
|
|
341
|
-
key={col.phase}
|
|
342
|
-
className="group/board-col flex min-h-0 flex-col rounded-xl border border-border bg-muted/30"
|
|
343
|
-
>
|
|
344
|
-
{boardColumnMenu ? (
|
|
345
|
-
<BoardPhaseColumnHeader
|
|
346
|
-
label={col.label}
|
|
347
|
-
rawCount={rawInPhase.length}
|
|
348
|
-
filteredCount={cards.length}
|
|
349
|
-
searchValue={phaseSearch[col.phase]}
|
|
350
|
-
onSearchChange={v => setPhaseSearch(prev => ({ ...prev, [col.phase]: v }))}
|
|
351
|
-
menu={boardColumnMenu}
|
|
352
|
-
showLabels={bd.showColumnLabels}
|
|
353
|
-
showCounts={bd.showColumnCounts}
|
|
354
|
-
/>
|
|
355
|
-
) : (
|
|
356
|
-
<div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2.5">
|
|
357
|
-
{bd.showColumnLabels ? (
|
|
358
|
-
<p className="min-w-0 truncate text-sm font-semibold text-foreground">{col.label}</p>
|
|
359
|
-
) : (
|
|
360
|
-
<span className="min-w-0 flex-1" aria-hidden />
|
|
361
|
-
)}
|
|
362
|
-
{bd.showColumnCounts ? (
|
|
363
|
-
<Badge
|
|
364
|
-
variant="outline"
|
|
365
|
-
className="inline-flex h-6 min-w-6 shrink-0 items-center justify-center border-0 bg-muted/70 px-2 text-xs font-semibold tabular-nums text-foreground"
|
|
366
|
-
aria-label={`${rawInPhase.length} ${rawInPhase.length === 1 ? "record" : "records"}`}
|
|
367
|
-
>
|
|
368
|
-
{rawInPhase.length}
|
|
369
|
-
</Badge>
|
|
370
|
-
) : null}
|
|
371
|
-
</div>
|
|
372
|
-
)}
|
|
373
|
-
<div className="flex flex-1 flex-col gap-2 overflow-y-auto p-2">
|
|
374
|
-
{bd.newCardAbove ? <BoardNewCardPlaceholder position="above" /> : null}
|
|
375
|
-
{cards.length === 0 ? (
|
|
376
|
-
<p className="px-2 py-6 text-center text-xs text-muted-foreground">No placements</p>
|
|
377
|
-
) : (
|
|
378
|
-
cards.map(row => (
|
|
379
|
-
<BoardPlacementCard
|
|
380
|
-
key={row.id}
|
|
381
|
-
row={row}
|
|
382
|
-
hiddenColKeys={hiddenColKeys}
|
|
383
|
-
lineCount={bd.lineCount}
|
|
384
|
-
conditionalRules={conditionalRules}
|
|
385
|
-
boardColumns={boardColumns}
|
|
386
|
-
onOpen={id => router.push(`/data-list/${id}`)}
|
|
387
|
-
/>
|
|
388
|
-
))
|
|
389
|
-
)}
|
|
390
|
-
</div>
|
|
391
|
-
</div>
|
|
392
|
-
)
|
|
393
|
-
})}
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
)
|
|
397
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
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.
|
|
7
|
-
*
|
|
8
|
-
* Uses centralized exports from `@/components/data-views`.
|
|
9
|
-
*
|
|
10
|
-
* NOTE: Lifecycle (all/upcoming/ongoing/completed) segments have been removed.
|
|
11
|
-
* All tabs read the same row bag; users can create their own segments via
|
|
12
|
-
* "Add view" / filters.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import * as React from "react"
|
|
16
|
-
import { useRouter } from "next/navigation"
|
|
17
|
-
import { useSidebar } from "@/components/ui/sidebar"
|
|
18
|
-
import {
|
|
19
|
-
ListPageTemplate,
|
|
20
|
-
type ViewTab,
|
|
21
|
-
PlacementsTable,
|
|
22
|
-
type PlacementsTableHandle,
|
|
23
|
-
type DataListViewType,
|
|
24
|
-
dataListViewIcon,
|
|
25
|
-
} from "@/components/data-views"
|
|
26
|
-
import { PlacementsPageHeader } from "@/components/placements-page-header"
|
|
27
|
-
import {
|
|
28
|
-
DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
|
|
29
|
-
type DataListDisplayOptions,
|
|
30
|
-
} from "@/lib/data-list-display-options"
|
|
31
|
-
import { loadPageFromStorage, schedulePageSave } from "@/lib/data-list-persistence"
|
|
32
|
-
import { KeyMetrics } from "@/components/key-metrics"
|
|
33
|
-
import { ALL_PLACEMENTS } from "@/lib/mock/placements"
|
|
34
|
-
import { PLACEMENT_KPI_INSIGHT, PLACEMENT_KPI_METRICS } from "@/lib/mock/placements-kpi"
|
|
35
|
-
import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
|
|
36
|
-
import { CoachMark } from "@/components/ui/coach-mark"
|
|
37
|
-
import { useCoachMark, type CoachMarkStep } from "@/hooks/use-coach-mark"
|
|
38
|
-
|
|
39
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
-
// Coach mark flow — Views & Properties tour
|
|
41
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
const VIEWS_TOUR_STEPS: CoachMarkStep[] = [
|
|
44
|
-
{
|
|
45
|
-
id: "views-tabs",
|
|
46
|
-
target: "[role='toolbar'][aria-label='Views']",
|
|
47
|
-
side: "bottom",
|
|
48
|
-
align: "start",
|
|
49
|
-
title: "Switch Between Views",
|
|
50
|
-
description:
|
|
51
|
-
"Use these tabs to move between saved views. Each tab keeps its own layout, filters, and properties.",
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: "views-settings",
|
|
55
|
-
target: "[aria-label='View settings']",
|
|
56
|
-
side: "bottom",
|
|
57
|
-
align: "start",
|
|
58
|
-
title: "Customise Each View",
|
|
59
|
-
description:
|
|
60
|
-
"Click the dropdown arrow to rename, duplicate, or edit a view. Choose between Table, List, Board, or Dashboard layouts.",
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "views-add",
|
|
64
|
-
target: "button:has(.fa-plus) + .fa-plus, [aria-label='Views'] ~ button",
|
|
65
|
-
side: "bottom",
|
|
66
|
-
align: "start",
|
|
67
|
-
title: "Create New Views",
|
|
68
|
-
description:
|
|
69
|
-
"Add custom views with different layouts and filters. Create a Board view for visual tracking, or a Dashboard for charts and KPIs.",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
id: "views-search",
|
|
73
|
-
target: "button[aria-label='Search']",
|
|
74
|
-
side: "bottom",
|
|
75
|
-
align: "end",
|
|
76
|
-
title: "Quick Search",
|
|
77
|
-
description:
|
|
78
|
-
"Instantly search across all visible columns. Press ⌘K to open search from anywhere on the page.",
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
id: "views-filter",
|
|
82
|
-
target: "button[aria-label='Add filter']:last-of-type, button:has(.fa-filter-list)",
|
|
83
|
-
side: "bottom",
|
|
84
|
-
align: "end",
|
|
85
|
-
title: "Filter Your Data",
|
|
86
|
-
description:
|
|
87
|
-
"Add filters to narrow down results. Combine multiple conditions — filter by status, date, site, program, and more.",
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
id: "views-properties",
|
|
91
|
-
target: "button[aria-label='Properties']",
|
|
92
|
-
side: "bottom",
|
|
93
|
-
align: "end",
|
|
94
|
-
title: "Table Properties",
|
|
95
|
-
description:
|
|
96
|
-
"Open the Properties panel to manage columns, sort order, conditional formatting, density, and gridlines. Everything is saved per view.",
|
|
97
|
-
},
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
-
// Config
|
|
102
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
const DEFAULT_TABS: ViewTab[] = [
|
|
105
|
-
{ id: "all", label: "All", viewType: "table", icon: "fa-table", filterId: "all" },
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
-
// Component
|
|
110
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
export function PlacementsClient() {
|
|
113
|
-
const router = useRouter()
|
|
114
|
-
const { setOpen } = useSidebar()
|
|
115
|
-
const [showMetrics, setShowMetrics] = React.useState(true)
|
|
116
|
-
const [exportOpen, setExportOpen] = React.useState(false)
|
|
117
|
-
const [displayOptions, setDisplayOptions] = React.useState<DataListDisplayOptions>(DEFAULT_DATA_LIST_DISPLAY_OPTIONS)
|
|
118
|
-
const [tabs, setTabs] = React.useState<ViewTab[]>(DEFAULT_TABS)
|
|
119
|
-
const [activeTabId, setActiveTabId] = React.useState<string>(DEFAULT_TABS[0]?.id ?? "")
|
|
120
|
-
const tableRef = React.useRef<PlacementsTableHandle>(null)
|
|
121
|
-
|
|
122
|
-
const viewsTour = useCoachMark({
|
|
123
|
-
flowId: "data-list-views-tour",
|
|
124
|
-
steps: VIEWS_TOUR_STEPS,
|
|
125
|
-
delay: 1200,
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
const activeTab = tabs.find((t) => t.id === activeTabId)
|
|
129
|
-
const placementCount = ALL_PLACEMENTS.length
|
|
130
|
-
|
|
131
|
-
useAskLeoPageContext(
|
|
132
|
-
React.useMemo(
|
|
133
|
-
() => ({
|
|
134
|
-
title: "List hub",
|
|
135
|
-
description: activeTab
|
|
136
|
-
? `${placementCount} row${placementCount === 1 ? "" : "s"} in “${activeTab.label}” · ${activeTab.viewType} view.`
|
|
137
|
-
: undefined,
|
|
138
|
-
suggestions: [
|
|
139
|
-
"Which rows are due in the next 30 days?",
|
|
140
|
-
"Summarize what is visible after my filters",
|
|
141
|
-
"What columns help reviewers scan this grid quickly?",
|
|
142
|
-
],
|
|
143
|
-
}),
|
|
144
|
-
[activeTab, placementCount],
|
|
145
|
-
),
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
React.useLayoutEffect(() => {
|
|
149
|
-
const p = loadPageFromStorage()
|
|
150
|
-
if (!p) return
|
|
151
|
-
setDisplayOptions({ ...DEFAULT_DATA_LIST_DISPLAY_OPTIONS, ...p.displayOptions })
|
|
152
|
-
setShowMetrics(p.showMetrics)
|
|
153
|
-
setTabs(p.tabs)
|
|
154
|
-
const nextActive = p.tabs.some(t => t.id === p.activeTabId) ? p.activeTabId : (p.tabs[0]?.id ?? "")
|
|
155
|
-
setActiveTabId(nextActive)
|
|
156
|
-
}, [])
|
|
157
|
-
|
|
158
|
-
React.useEffect(() => {
|
|
159
|
-
schedulePageSave({
|
|
160
|
-
v: 1,
|
|
161
|
-
displayOptions,
|
|
162
|
-
showMetrics,
|
|
163
|
-
tabs,
|
|
164
|
-
activeTabId,
|
|
165
|
-
})
|
|
166
|
-
}, [displayOptions, showMetrics, tabs, activeTabId])
|
|
167
|
-
|
|
168
|
-
function handleNewPlacement() {
|
|
169
|
-
setOpen(false)
|
|
170
|
-
setTimeout(() => router.push("/data-list/new"), 260)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<>
|
|
175
|
-
<CoachMark state={viewsTour} />
|
|
176
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
177
|
-
<ListPageTemplate
|
|
178
|
-
tabs={tabs}
|
|
179
|
-
onTabsChange={setTabs}
|
|
180
|
-
activeTabId={activeTabId}
|
|
181
|
-
onActiveTabChange={setActiveTabId}
|
|
182
|
-
tablePropertiesRef={tableRef}
|
|
183
|
-
header={
|
|
184
|
-
<PlacementsPageHeader
|
|
185
|
-
onNewPlacement={handleNewPlacement}
|
|
186
|
-
onExport={() => setExportOpen(true)}
|
|
187
|
-
showMetrics={showMetrics}
|
|
188
|
-
onToggleMetrics={() => setShowMetrics(v => !v)}
|
|
189
|
-
showTitleBlock={displayOptions.showViewTitle}
|
|
190
|
-
/>
|
|
191
|
-
}
|
|
192
|
-
metrics={
|
|
193
|
-
<KeyMetrics
|
|
194
|
-
variant="flat"
|
|
195
|
-
metrics={PLACEMENT_KPI_METRICS}
|
|
196
|
-
insight={PLACEMENT_KPI_INSIGHT}
|
|
197
|
-
showHeader={false}
|
|
198
|
-
metricsSingleRow
|
|
199
|
-
/>
|
|
200
|
-
}
|
|
201
|
-
showMetrics={showMetrics}
|
|
202
|
-
defaultTabs={DEFAULT_TABS}
|
|
203
|
-
renderContent={(tab, updateTab) => (
|
|
204
|
-
<PlacementsTable
|
|
205
|
-
key={tab.id}
|
|
206
|
-
ref={tableRef}
|
|
207
|
-
view={tab.viewType}
|
|
208
|
-
onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
|
|
209
|
-
displayOptions={displayOptions}
|
|
210
|
-
onDisplayOptionsChange={patch =>
|
|
211
|
-
setDisplayOptions(prev => ({ ...prev, ...patch }))}
|
|
212
|
-
/>
|
|
213
|
-
)}
|
|
214
|
-
exportOpen={exportOpen}
|
|
215
|
-
onExportOpenChange={setExportOpen}
|
|
216
|
-
/>
|
|
217
|
-
</div>
|
|
218
|
-
</>
|
|
219
|
-
)
|
|
220
|
-
}
|