@exxatdesignux/ui 0.3.0 → 0.4.0
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 +608 -6
- 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/banner.d.ts +2 -2
- 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 +1 -1
- 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/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/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,553 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Team roster — thin wrapper around the centralized `<HubTable>`. Owns only the column defs,
|
|
5
|
-
* panel-view helpers, dashboard layout state, and per-view renderers.
|
|
6
|
-
*
|
|
7
|
-
* Single dataset: `HubTable` runs one `useTableState` and every renderer (list, board, panel,
|
|
8
|
-
* dashboard) reads `state.rows` (filtered/sorted). KPIs and panel groups derive from those.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as React from "react"
|
|
12
|
-
import { useRouter } from "next/navigation"
|
|
13
|
-
import { AvatarInitials } from "@/components/ui/avatar"
|
|
14
|
-
import {
|
|
15
|
-
TEAM_MEMBER_STATUS_BADGE_CLASS,
|
|
16
|
-
TEAM_MEMBER_STATUS_ICON,
|
|
17
|
-
TEAM_MEMBER_STATUS_LABEL,
|
|
18
|
-
} from "@/lib/list-status-badges"
|
|
19
|
-
import { mailtoHref } from "@/lib/mailto"
|
|
20
|
-
import type { TeamMember } from "@/lib/mock/team"
|
|
21
|
-
import { DataTableToolbar } from "@/components/data-table"
|
|
22
|
-
import {
|
|
23
|
-
TeamDashboardChartsSection,
|
|
24
|
-
DEFAULT_TEAM_CHART_TYPES,
|
|
25
|
-
DEFAULT_TEAM_SPANS,
|
|
26
|
-
ALL_TEAM_DASHBOARD_CARDS,
|
|
27
|
-
loadTeamDashboardLayout,
|
|
28
|
-
mergeTeamDashboardLayout,
|
|
29
|
-
saveTeamDashboardLayout,
|
|
30
|
-
} from "@/components/data-view-dashboard-charts-team"
|
|
31
|
-
import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
|
|
32
|
-
import type { ChartType, DashboardLayout } from "@/lib/data-view-dashboard-placements-layout"
|
|
33
|
-
import { ListPageBoardCard, ListPageBoardCardAvatar } from "@/components/data-views/list-page-board-card"
|
|
34
|
-
import { TeamBoardView, TEAM_BOARD_GROUP_OPTIONS } from "@/components/team-board-view"
|
|
35
|
-
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
36
|
-
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
37
|
-
import { teamKpiInsight, teamKpiMetrics } from "@/lib/mock/team-kpi"
|
|
38
|
-
import { cn } from "@/lib/utils"
|
|
39
|
-
import type { DataListViewType } from "@/lib/data-list-view"
|
|
40
|
-
import type { ColumnDef } from "@/components/data-table/types"
|
|
41
|
-
import { TablePropertiesDrawerButton } from "@/components/table-properties"
|
|
42
|
-
import {
|
|
43
|
-
HubTable,
|
|
44
|
-
type HubTableHandle,
|
|
45
|
-
type HubTableRenderers,
|
|
46
|
-
type HubTableRendererArgs,
|
|
47
|
-
} from "@/components/data-views"
|
|
48
|
-
import { TEAM_SUPPORTED_VIEWS } from "@/lib/team-supported-views"
|
|
49
|
-
import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
|
|
50
|
-
import { Button } from "@/components/ui/button"
|
|
51
|
-
import {
|
|
52
|
-
DropdownMenu,
|
|
53
|
-
DropdownMenuContent,
|
|
54
|
-
DropdownMenuItem,
|
|
55
|
-
DropdownMenuTrigger,
|
|
56
|
-
} from "@/components/ui/dropdown-menu"
|
|
57
|
-
import { Tip } from "@/components/ui/tip"
|
|
58
|
-
import { CoachMark } from "@/components/ui/coach-mark"
|
|
59
|
-
import { useCoachMark } from "@/hooks/use-coach-mark"
|
|
60
|
-
import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
|
|
61
|
-
|
|
62
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
function uniqueRoles(members: TeamMember[]) {
|
|
65
|
-
return [...new Set(members.map(m => m.role))].sort().map(r => ({ value: r, label: r }))
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function formatUsPhoneDigits(digits: string) {
|
|
69
|
-
const d = digits.replace(/\D/g, "").slice(0, 10)
|
|
70
|
-
if (d.length !== 10) return digits
|
|
71
|
-
return `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const STATUS_FILTER_OPTS = [
|
|
75
|
-
{ value: "active", label: TEAM_MEMBER_STATUS_LABEL.active },
|
|
76
|
-
{ value: "away", label: TEAM_MEMBER_STATUS_LABEL.away },
|
|
77
|
-
{ value: "invited", label: TEAM_MEMBER_STATUS_LABEL.invited },
|
|
78
|
-
]
|
|
79
|
-
|
|
80
|
-
const TEAM_STATUS_GROUPS: Array<{ id: string; label: string; accent: string }> = [
|
|
81
|
-
{ id: "all", label: "All", accent: "bg-muted-foreground" },
|
|
82
|
-
{ id: "active", label: "Active", accent: "bg-success" },
|
|
83
|
-
{ id: "away", label: "Away", accent: "bg-warning" },
|
|
84
|
-
{ id: "invited", label: "Invited", accent: "bg-brand" },
|
|
85
|
-
]
|
|
86
|
-
|
|
87
|
-
function buildTeamStatusGroups(members: TeamMember[]): FinderGroup[] {
|
|
88
|
-
return TEAM_STATUS_GROUPS.map(sg => ({
|
|
89
|
-
id: sg.id,
|
|
90
|
-
label: sg.label,
|
|
91
|
-
accent: sg.accent,
|
|
92
|
-
count: sg.id === "all" ? members.length : members.filter(m => m.status === sg.id).length,
|
|
93
|
-
}))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ─── Team-specific panel view rows ───────────────────────────────────────────
|
|
97
|
-
|
|
98
|
-
function TeamFinderListRow({ member, isSelected }: { member: TeamMember; isSelected: boolean }) {
|
|
99
|
-
return (
|
|
100
|
-
<div
|
|
101
|
-
className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
|
|
102
|
-
isSelected ? "bg-transparent text-accent-foreground" : "text-foreground"
|
|
103
|
-
}`}
|
|
104
|
-
>
|
|
105
|
-
<AvatarInitials
|
|
106
|
-
initials={member.initials}
|
|
107
|
-
className={cn(
|
|
108
|
-
"size-8 shrink-0 rounded-full text-[11px] font-semibold",
|
|
109
|
-
isSelected ? "ring-2 ring-accent-foreground/35" : "",
|
|
110
|
-
)}
|
|
111
|
-
/>
|
|
112
|
-
<div className="min-w-0 flex-1">
|
|
113
|
-
<p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
|
|
114
|
-
{member.name}
|
|
115
|
-
</p>
|
|
116
|
-
<p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
|
|
117
|
-
{member.role}
|
|
118
|
-
</p>
|
|
119
|
-
</div>
|
|
120
|
-
{!isSelected && (
|
|
121
|
-
<ListHubStatusBadge
|
|
122
|
-
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
123
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
124
|
-
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
125
|
-
/>
|
|
126
|
-
)}
|
|
127
|
-
</div>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function TeamFinderDetail({ member }: { member: TeamMember }) {
|
|
132
|
-
const router = useRouter()
|
|
133
|
-
return (
|
|
134
|
-
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
135
|
-
<div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
|
|
136
|
-
<AvatarInitials initials={member.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
|
|
137
|
-
<div className="min-w-0 flex-1">
|
|
138
|
-
<h2 className="text-base font-semibold text-foreground leading-tight">{member.name}</h2>
|
|
139
|
-
<p className="mt-0.5 text-[13px] text-muted-foreground">{member.role}</p>
|
|
140
|
-
<div className="mt-2">
|
|
141
|
-
<ListHubStatusBadge
|
|
142
|
-
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
143
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
144
|
-
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
145
|
-
/>
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
<Tip side="bottom" label="Open full profile">
|
|
149
|
-
<Button
|
|
150
|
-
type="button"
|
|
151
|
-
variant="outline"
|
|
152
|
-
size="sm"
|
|
153
|
-
className="shrink-0"
|
|
154
|
-
onClick={() => router.push(`/team/${member.id}`)}
|
|
155
|
-
aria-label={`Open full profile for ${member.name}`}
|
|
156
|
-
>
|
|
157
|
-
<i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
|
|
158
|
-
Open
|
|
159
|
-
</Button>
|
|
160
|
-
</Tip>
|
|
161
|
-
</div>
|
|
162
|
-
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
163
|
-
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
164
|
-
<div className="flex flex-col gap-0.5">
|
|
165
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
166
|
-
<i className="fa-light fa-envelope text-[10px]" aria-hidden="true" /> Email
|
|
167
|
-
</dt>
|
|
168
|
-
<dd className="text-[13px]">
|
|
169
|
-
<a href={mailtoHref(member.email)} className="text-interactive-foreground hover:underline">
|
|
170
|
-
{member.email}
|
|
171
|
-
</a>
|
|
172
|
-
</dd>
|
|
173
|
-
</div>
|
|
174
|
-
<div className="flex flex-col gap-0.5">
|
|
175
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
176
|
-
<i className="fa-light fa-briefcase text-[10px]" aria-hidden="true" /> Role
|
|
177
|
-
</dt>
|
|
178
|
-
<dd className="text-[13px] text-foreground">{member.role}</dd>
|
|
179
|
-
</div>
|
|
180
|
-
<div className="flex flex-col gap-0.5">
|
|
181
|
-
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
182
|
-
<i className="fa-light fa-phone text-[10px]" aria-hidden="true" /> Phone
|
|
183
|
-
</dt>
|
|
184
|
-
<dd className="text-[13px] text-foreground">{member.phone}</dd>
|
|
185
|
-
</div>
|
|
186
|
-
</dl>
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ─── Columns ─────────────────────────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
function buildTeamColumns(members: TeamMember[]): ColumnDef<TeamMember>[] {
|
|
195
|
-
const roleOpts = uniqueRoles(members)
|
|
196
|
-
return [
|
|
197
|
-
{ key: "select", label: "", width: 40, minWidth: 40, defaultPin: "left", lockPin: true },
|
|
198
|
-
{
|
|
199
|
-
key: "name",
|
|
200
|
-
label: "Name",
|
|
201
|
-
width: 240,
|
|
202
|
-
minWidth: 160,
|
|
203
|
-
sortable: true,
|
|
204
|
-
sortKey: "name",
|
|
205
|
-
defaultPin: "left",
|
|
206
|
-
filter: { type: "text", icon: "fa-user", operators: ["contains", "not_contains"] },
|
|
207
|
-
cell: row => (
|
|
208
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
209
|
-
<AvatarInitials initials={row.initials} className="size-8 shrink-0 text-xs" />
|
|
210
|
-
<span className="truncate text-sm font-medium text-foreground">{row.name}</span>
|
|
211
|
-
</div>
|
|
212
|
-
),
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
key: "role",
|
|
216
|
-
label: "Role",
|
|
217
|
-
width: 200,
|
|
218
|
-
minWidth: 140,
|
|
219
|
-
sortable: true,
|
|
220
|
-
sortKey: "role",
|
|
221
|
-
filter: { type: "select", icon: "fa-briefcase", operators: ["is", "is_not"], options: roleOpts },
|
|
222
|
-
cell: row => <span className="text-sm text-foreground/90">{row.role}</span>,
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
key: "email",
|
|
226
|
-
label: "Email",
|
|
227
|
-
width: 260,
|
|
228
|
-
minWidth: 180,
|
|
229
|
-
sortable: true,
|
|
230
|
-
sortKey: "email",
|
|
231
|
-
filter: { type: "text", icon: "fa-envelope", operators: ["contains", "not_contains"] },
|
|
232
|
-
cell: row => (
|
|
233
|
-
<a href={mailtoHref(row.email)} className="text-sm text-primary hover:underline truncate block">
|
|
234
|
-
{row.email}
|
|
235
|
-
</a>
|
|
236
|
-
),
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
key: "phone",
|
|
240
|
-
label: "Phone",
|
|
241
|
-
width: 148,
|
|
242
|
-
minWidth: 132,
|
|
243
|
-
sortable: true,
|
|
244
|
-
sortKey: "phone",
|
|
245
|
-
filter: { type: "text", icon: "fa-phone", operators: ["contains", "not_contains"], textMask: "phone" },
|
|
246
|
-
cell: row => (
|
|
247
|
-
<a
|
|
248
|
-
href={`tel:+1${row.phone}`}
|
|
249
|
-
className="text-sm tabular-nums text-foreground/90 hover:text-primary hover:underline truncate block"
|
|
250
|
-
>
|
|
251
|
-
{formatUsPhoneDigits(row.phone)}
|
|
252
|
-
</a>
|
|
253
|
-
),
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
key: "status",
|
|
257
|
-
label: "Status",
|
|
258
|
-
width: 120,
|
|
259
|
-
minWidth: 100,
|
|
260
|
-
sortable: true,
|
|
261
|
-
sortKey: "status",
|
|
262
|
-
filter: { type: "select", icon: "fa-circle-dot", operators: ["is", "is_not"], options: STATUS_FILTER_OPTS },
|
|
263
|
-
cell: row => (
|
|
264
|
-
<ListHubStatusBadge
|
|
265
|
-
label={TEAM_MEMBER_STATUS_LABEL[row.status]}
|
|
266
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[row.status]}
|
|
267
|
-
icon={TEAM_MEMBER_STATUS_ICON[row.status]}
|
|
268
|
-
/>
|
|
269
|
-
),
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
key: "actions",
|
|
273
|
-
label: "",
|
|
274
|
-
width: 48,
|
|
275
|
-
minWidth: 48,
|
|
276
|
-
defaultPin: "right",
|
|
277
|
-
lockPin: true,
|
|
278
|
-
cell: row => (
|
|
279
|
-
<div className="flex items-center justify-center">
|
|
280
|
-
<DropdownMenu>
|
|
281
|
-
<DropdownMenuTrigger asChild>
|
|
282
|
-
<Button size="icon-sm" variant="ghost" aria-label={`Actions for ${row.name}`}>
|
|
283
|
-
<i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
|
|
284
|
-
</Button>
|
|
285
|
-
</DropdownMenuTrigger>
|
|
286
|
-
<DropdownMenuContent align="end">
|
|
287
|
-
<DropdownMenuItem onClick={() => window.open(mailtoHref(row.email))}>
|
|
288
|
-
<i className="fa-light fa-envelope" aria-hidden="true" />
|
|
289
|
-
Email
|
|
290
|
-
</DropdownMenuItem>
|
|
291
|
-
<DropdownMenuItem disabled>
|
|
292
|
-
<i className="fa-light fa-user-gear" aria-hidden="true" />
|
|
293
|
-
Manage access
|
|
294
|
-
</DropdownMenuItem>
|
|
295
|
-
</DropdownMenuContent>
|
|
296
|
-
</DropdownMenu>
|
|
297
|
-
</div>
|
|
298
|
-
),
|
|
299
|
-
},
|
|
300
|
-
]
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ─── Dashboard body (split out so it can use hooks inside renderer closure) ─
|
|
304
|
-
|
|
305
|
-
interface TeamDashboardBodyProps {
|
|
306
|
-
args: HubTableRendererArgs<TeamMember>
|
|
307
|
-
columns: ColumnDef<TeamMember>[]
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function TeamDashboardBody({ args, columns }: TeamDashboardBodyProps) {
|
|
311
|
-
const { state, drawerToolbarProps, displayOptions } = args
|
|
312
|
-
const rows = state.rows as TeamMember[]
|
|
313
|
-
|
|
314
|
-
const dashboardKpi = React.useMemo(
|
|
315
|
-
() => ({ metrics: teamKpiMetrics(rows), insight: teamKpiInsight(rows) }),
|
|
316
|
-
[rows],
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
const [visibleCards, setVisibleCards] = React.useState<string[]>(() => ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
320
|
-
const [cardOrder, setCardOrder] = React.useState<string[]>(() => ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
321
|
-
const [cardSpans, setCardSpans] = React.useState<Record<string, 1 | 2>>(() => ({ ...DEFAULT_TEAM_SPANS }))
|
|
322
|
-
const [cardChartTypes, setCardChartTypes] = React.useState<Record<string, ChartType>>(() => ({ ...DEFAULT_TEAM_CHART_TYPES }))
|
|
323
|
-
const [kpiCount, setKpiCount] = React.useState<number>(KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
324
|
-
const [layoutEdit, setLayoutEdit] = React.useState(false)
|
|
325
|
-
const hydrated = React.useRef(false)
|
|
326
|
-
const baselineRef = React.useRef<DashboardLayout | null>(null)
|
|
327
|
-
|
|
328
|
-
React.useEffect(() => {
|
|
329
|
-
const saved = loadTeamDashboardLayout()
|
|
330
|
-
const m = mergeTeamDashboardLayout(saved)
|
|
331
|
-
setVisibleCards(m.visible)
|
|
332
|
-
setCardOrder(m.order)
|
|
333
|
-
setCardSpans(m.spans ?? { ...DEFAULT_TEAM_SPANS })
|
|
334
|
-
setCardChartTypes(m.chartTypes ?? { ...DEFAULT_TEAM_CHART_TYPES })
|
|
335
|
-
setKpiCount(m.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
336
|
-
hydrated.current = true
|
|
337
|
-
}, [])
|
|
338
|
-
|
|
339
|
-
React.useEffect(() => {
|
|
340
|
-
if (!hydrated.current) return
|
|
341
|
-
saveTeamDashboardLayout({
|
|
342
|
-
visible: visibleCards,
|
|
343
|
-
order: cardOrder,
|
|
344
|
-
spans: cardSpans,
|
|
345
|
-
chartTypes: cardChartTypes,
|
|
346
|
-
keyMetricsKpiCount: kpiCount,
|
|
347
|
-
})
|
|
348
|
-
}, [visibleCards, cardOrder, cardSpans, cardChartTypes, kpiCount])
|
|
349
|
-
|
|
350
|
-
const onResetLayout = React.useCallback(() => {
|
|
351
|
-
setVisibleCards(ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
352
|
-
setCardOrder(ALL_TEAM_DASHBOARD_CARDS.map(c => c.id))
|
|
353
|
-
setCardSpans({ ...DEFAULT_TEAM_SPANS })
|
|
354
|
-
setCardChartTypes({ ...DEFAULT_TEAM_CHART_TYPES })
|
|
355
|
-
setKpiCount(KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
356
|
-
}, [])
|
|
357
|
-
|
|
358
|
-
const onLayoutEditStart = React.useCallback(() => {
|
|
359
|
-
baselineRef.current = {
|
|
360
|
-
visible: [...visibleCards],
|
|
361
|
-
order: [...cardOrder],
|
|
362
|
-
spans: { ...cardSpans },
|
|
363
|
-
chartTypes: { ...cardChartTypes },
|
|
364
|
-
keyMetricsKpiCount: kpiCount,
|
|
365
|
-
}
|
|
366
|
-
setLayoutEdit(true)
|
|
367
|
-
}, [visibleCards, cardOrder, cardSpans, cardChartTypes, kpiCount])
|
|
368
|
-
|
|
369
|
-
const onLayoutEditCancel = React.useCallback(() => {
|
|
370
|
-
const b = baselineRef.current
|
|
371
|
-
if (b) {
|
|
372
|
-
setVisibleCards(b.visible)
|
|
373
|
-
setCardOrder(b.order)
|
|
374
|
-
setCardSpans(b.spans ?? { ...DEFAULT_TEAM_SPANS })
|
|
375
|
-
setCardChartTypes(b.chartTypes ?? { ...DEFAULT_TEAM_CHART_TYPES })
|
|
376
|
-
setKpiCount(b.keyMetricsKpiCount ?? KEY_METRICS_KPI_COUNT_DEFAULT)
|
|
377
|
-
}
|
|
378
|
-
setLayoutEdit(false)
|
|
379
|
-
}, [])
|
|
380
|
-
|
|
381
|
-
const coach = useCoachMark({
|
|
382
|
-
flowId: "team-dashboard-customize",
|
|
383
|
-
steps: DASHBOARD_CUSTOMIZE_COACH_STEPS,
|
|
384
|
-
delay: 700,
|
|
385
|
-
enabled: true,
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
return (
|
|
389
|
-
<div className="flex min-h-0 flex-1 flex-col">
|
|
390
|
-
<CoachMark state={coach} />
|
|
391
|
-
{!layoutEdit ? (
|
|
392
|
-
<DataTableToolbar
|
|
393
|
-
state={state}
|
|
394
|
-
columns={columns}
|
|
395
|
-
searchable={displayOptions.showToolbarSearch}
|
|
396
|
-
searchAriaLabel="Search team members"
|
|
397
|
-
toolbarSlot={s => (
|
|
398
|
-
<TablePropertiesDrawerButton
|
|
399
|
-
{...drawerToolbarProps}
|
|
400
|
-
state={s}
|
|
401
|
-
extraActions={
|
|
402
|
-
<Tip side="bottom" label="Edit dashboard layout on canvas">
|
|
403
|
-
<Button
|
|
404
|
-
type="button"
|
|
405
|
-
variant="ghost"
|
|
406
|
-
size="icon-sm"
|
|
407
|
-
aria-label="Edit dashboard layout"
|
|
408
|
-
onClick={onLayoutEditStart}
|
|
409
|
-
className="text-muted-foreground hover:text-interactive-hover-foreground hover:bg-interactive-hover"
|
|
410
|
-
>
|
|
411
|
-
<i className="fa-light fa-pen-ruler text-[13px]" aria-hidden="true" />
|
|
412
|
-
</Button>
|
|
413
|
-
</Tip>
|
|
414
|
-
}
|
|
415
|
-
/>
|
|
416
|
-
)}
|
|
417
|
-
/>
|
|
418
|
-
) : null}
|
|
419
|
-
<TeamDashboardChartsSection
|
|
420
|
-
members={rows}
|
|
421
|
-
keyMetrics={dashboardKpi}
|
|
422
|
-
visibleCards={visibleCards}
|
|
423
|
-
cardOrder={cardOrder}
|
|
424
|
-
cardSpans={cardSpans}
|
|
425
|
-
cardChartTypes={cardChartTypes}
|
|
426
|
-
keyMetricsKpiCount={kpiCount}
|
|
427
|
-
layoutEditMode={layoutEdit}
|
|
428
|
-
onVisibleChange={setVisibleCards}
|
|
429
|
-
onOrderChange={setCardOrder}
|
|
430
|
-
onSpanChange={(id, span) => setCardSpans(prev => ({ ...prev, [id]: span }))}
|
|
431
|
-
onChartTypeChange={(id, t) => setCardChartTypes(prev => ({ ...prev, [id]: t }))}
|
|
432
|
-
onKeyMetricsKpiCountChange={setKpiCount}
|
|
433
|
-
onResetLayout={onResetLayout}
|
|
434
|
-
onLayoutEditDone={() => setLayoutEdit(false)}
|
|
435
|
-
onLayoutEditCancel={onLayoutEditCancel}
|
|
436
|
-
/>
|
|
437
|
-
</div>
|
|
438
|
-
)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// ─── Public component ───────────────────────────────────────────────────────
|
|
442
|
-
|
|
443
|
-
export type TeamTableHandle = HubTableHandle
|
|
444
|
-
|
|
445
|
-
export const TeamTable = React.forwardRef<
|
|
446
|
-
TeamTableHandle,
|
|
447
|
-
{ members: TeamMember[]; view?: DataListViewType; onViewChange?: (v: DataListViewType) => void }
|
|
448
|
-
>(function TeamTable({ members, view = "table", onViewChange }, ref) {
|
|
449
|
-
const columns = React.useMemo(() => buildTeamColumns(members), [members])
|
|
450
|
-
|
|
451
|
-
const renderers: HubTableRenderers<TeamMember> = {
|
|
452
|
-
"board-with-toolbar": ({ state, toolbarShell, displayOptions }) => {
|
|
453
|
-
const boardGroupKey = TEAM_BOARD_GROUP_OPTIONS.some(
|
|
454
|
-
o => o.key === displayOptions.boardGroupByColumnKey,
|
|
455
|
-
)
|
|
456
|
-
? displayOptions.boardGroupByColumnKey
|
|
457
|
-
: "status"
|
|
458
|
-
return toolbarShell(
|
|
459
|
-
<TeamBoardView
|
|
460
|
-
members={state.rows as TeamMember[]}
|
|
461
|
-
groupByColumnKey={boardGroupKey}
|
|
462
|
-
onRowActivate={m => state.toggleRow(m.id)}
|
|
463
|
-
/>,
|
|
464
|
-
)
|
|
465
|
-
},
|
|
466
|
-
"panel-with-toolbar": ({ state, toolbarShell }) => {
|
|
467
|
-
const groups = buildTeamStatusGroups(state.rows as TeamMember[])
|
|
468
|
-
return toolbarShell(
|
|
469
|
-
<ListPageSplitHubChrome aria-label="Team members panel view">
|
|
470
|
-
<FinderPanelView<TeamMember>
|
|
471
|
-
embedded
|
|
472
|
-
groupsColumnTitle="Status"
|
|
473
|
-
groups={groups}
|
|
474
|
-
rows={state.rows as TeamMember[]}
|
|
475
|
-
getRowId={r => r.id}
|
|
476
|
-
getRowGroupId={r => r.status}
|
|
477
|
-
defaultGroupId="all"
|
|
478
|
-
autoSaveId="team-panel-view"
|
|
479
|
-
ariaLabel="Team members panel view"
|
|
480
|
-
emptyList={<p>No team members found</p>}
|
|
481
|
-
renderListRow={(member, isSelected) => (
|
|
482
|
-
<TeamFinderListRow member={member} isSelected={isSelected} />
|
|
483
|
-
)}
|
|
484
|
-
renderDetail={member => <TeamFinderDetail member={member} />}
|
|
485
|
-
/>
|
|
486
|
-
</ListPageSplitHubChrome>,
|
|
487
|
-
)
|
|
488
|
-
},
|
|
489
|
-
"dashboard-with-toolbar": (args) => <TeamDashboardBody args={args} columns={columns} />,
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return (
|
|
493
|
-
<HubTable<TeamMember>
|
|
494
|
-
rows={members}
|
|
495
|
-
columns={columns}
|
|
496
|
-
view={view}
|
|
497
|
-
onViewChange={onViewChange}
|
|
498
|
-
supportedViewTypes={TEAM_SUPPORTED_VIEWS}
|
|
499
|
-
hubLabel="Team"
|
|
500
|
-
lifecycleTabLabel="Team"
|
|
501
|
-
searchAriaLabel="Search team members"
|
|
502
|
-
getRowId={row => row.id}
|
|
503
|
-
getRowSelectionLabel={row => row.name}
|
|
504
|
-
defaultSort={{ key: "name", dir: "asc" }}
|
|
505
|
-
emptyState={<p className="text-sm text-muted-foreground">No team members.</p>}
|
|
506
|
-
boardGroupByColumnOptions={[...TEAM_BOARD_GROUP_OPTIONS]}
|
|
507
|
-
listAriaLabel="Team members"
|
|
508
|
-
listEmptyState="No team members match your filters."
|
|
509
|
-
renderListRow={member => (
|
|
510
|
-
<ListPageBoardCard
|
|
511
|
-
layout="row"
|
|
512
|
-
rowContainerClassName="flex flex-row items-center gap-3"
|
|
513
|
-
leading={<ListPageBoardCardAvatar initials={member.initials} className="size-9" />}
|
|
514
|
-
rowEnd={
|
|
515
|
-
<div className="flex shrink-0 items-center gap-2">
|
|
516
|
-
<ListHubStatusBadge
|
|
517
|
-
surface="board"
|
|
518
|
-
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
519
|
-
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
520
|
-
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
521
|
-
/>
|
|
522
|
-
<i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden="true" />
|
|
523
|
-
</div>
|
|
524
|
-
}
|
|
525
|
-
>
|
|
526
|
-
<div className="space-y-0.5">
|
|
527
|
-
<p className="truncate text-sm font-semibold text-foreground">{member.name}</p>
|
|
528
|
-
<p className="text-xs text-muted-foreground">{member.role}</p>
|
|
529
|
-
<p className="truncate text-xs text-muted-foreground">{member.email}</p>
|
|
530
|
-
</div>
|
|
531
|
-
</ListPageBoardCard>
|
|
532
|
-
)}
|
|
533
|
-
bulkActionsSlot={selected => {
|
|
534
|
-
if (selected.size === 0) return null
|
|
535
|
-
return (
|
|
536
|
-
<>
|
|
537
|
-
<span className="sr-only">{selected.size} selected</span>
|
|
538
|
-
<Tip label="Export selection (demo)">
|
|
539
|
-
<Button size="sm" variant="outline" type="button">
|
|
540
|
-
<i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
|
|
541
|
-
Export
|
|
542
|
-
</Button>
|
|
543
|
-
</Tip>
|
|
544
|
-
</>
|
|
545
|
-
)
|
|
546
|
-
}}
|
|
547
|
-
renderers={renderers}
|
|
548
|
-
handleRef={ref}
|
|
549
|
-
/>
|
|
550
|
-
)
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
TeamTable.displayName = "TeamTable"
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Question bank hub header — folder scope + Customize folder
|
|
2
|
-
|
|
3
|
-
**Audience:** Engineers extending the question bank library hub (`QuestionBankClient`, `QuestionBankPageHeader`, URL scope).
|
|
4
|
-
|
|
5
|
-
## Problem
|
|
6
|
-
|
|
7
|
-
The library uses **`ListPageTemplate`** with multiple **view tabs** (table, panel, tree, …). **`QuestionBankNewFolderSheet`** (customize mode) is also used inside **`QuestionBankTable`** for some views (e.g. panel columns). If **Customize folder** exists only there, users on **table** or other tabs **cannot** open the sheet from a consistent chrome entry point when the URL is scoped to a folder (`?scope=folder&folderId=…`).
|
|
8
|
-
|
|
9
|
-
## Pattern
|
|
10
|
-
|
|
11
|
-
1. **`QuestionBankPageHeader`** exposes optional **`onCustomizeFolder?: () => void`**. When **`navState.scope === "folder"`** and **`navState.folderId`** is set, the hub client passes a callback that opens customize mode for the matching **`QuestionBankFolder`**.
|
|
12
|
-
2. **`QuestionBankClient`** (or equivalent hub client) mounts **`QuestionBankNewFolderSheet`** **once** beside **`SecondaryPanelHubTemplate` / `ListPageTemplate`**, with local state for **`open`** and **`customizingFolder`**. Saving updates **`folders`** the same way as table-embedded customize flows.
|
|
13
|
-
3. The header **⋯ More** menu order stays aligned with **§4.7**: **Invite people** (when collaboration variant) → **Customize folder** (when folder-scoped) → **Export** → **Show / hide metric section** (when applicable).
|
|
14
|
-
|
|
15
|
-
## References
|
|
16
|
-
|
|
17
|
-
| Piece | Location |
|
|
18
|
-
|-------|-----------|
|
|
19
|
-
| Header prop + menu item | `components/question-bank-page-header.tsx` |
|
|
20
|
-
| Client wiring + sheet | `components/question-bank-client.tsx` |
|
|
21
|
-
| URL scope | `lib/question-bank-nav.ts` (`parseQuestionBankNav`, `QuestionBankNavState`) |
|
|
22
|
-
| Sheet UI | `components/question-bank-new-folder-sheet.tsx` |
|
|
23
|
-
|
|
24
|
-
**Cursor rule:** `.cursor/rules/exxat-question-bank-hub-header.mdc`
|
|
25
|
-
**Handbook:** `AGENTS.md` §4.6 (folder-scoped hub chrome).
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { DataListViewType } from "@/lib/data-list-view"
|
|
2
|
-
|
|
3
|
-
/** Views implemented in `ComplianceTable` — keep in sync with the renderers passed to `HubTable`. */
|
|
4
|
-
export const COMPLIANCE_SUPPORTED_VIEWS = [
|
|
5
|
-
"table",
|
|
6
|
-
"list",
|
|
7
|
-
"board",
|
|
8
|
-
"panel",
|
|
9
|
-
"dashboard",
|
|
10
|
-
] as const satisfies readonly DataListViewType[]
|