@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.
Files changed (210) hide show
  1. package/CHANGELOG.md +608 -6
  2. package/consumer-extras/cursor-rules/exxat-board-cards.mdc +1 -1
  3. package/consumer-extras/cursor-rules/exxat-centralized-list-dataset.mdc +2 -2
  4. package/consumer-extras/cursor-rules/exxat-collaboration-access.mdc +1 -1
  5. package/consumer-extras/cursor-rules/exxat-data-tables.mdc +2 -0
  6. package/consumer-extras/cursor-rules/exxat-dedicated-search-surfaces.mdc +1 -1
  7. package/consumer-extras/cursor-rules/exxat-ds-agents.mdc +3 -3
  8. package/consumer-extras/cursor-rules/exxat-library-hub-header.mdc +28 -0
  9. package/consumer-extras/cursor-rules/exxat-mono-ids.mdc +1 -1
  10. package/consumer-extras/cursor-rules/exxat-person-identity-display.mdc +1 -1
  11. package/consumer-extras/cursor-rules/exxat-primary-nav-secondary-panel.mdc +6 -6
  12. package/consumer-extras/cursor-rules/exxat-reuse-before-custom.mdc +1 -1
  13. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +2 -2
  14. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +1 -1
  15. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +3 -3
  16. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +2 -2
  17. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +7 -7
  18. package/consumer-extras/cursor-skills/exxat-kpi-flat-band/SKILL.md +1 -1
  19. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +1 -1
  20. package/consumer-extras/cursor-skills/exxat-mono-ids/SKILL.md +4 -4
  21. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +8 -8
  22. package/consumer-extras/cursor-skills/exxat-token-economy/SKILL.md +277 -0
  23. package/consumer-extras/handbook/HANDBOOK.md +2 -0
  24. package/consumer-extras/handbook/glossary.md +2 -1
  25. package/consumer-extras/handbook/reference-implementations.md +31 -4
  26. package/consumer-extras/patterns/collaboration-access-pattern.md +7 -7
  27. package/consumer-extras/patterns/data-views-pattern.md +18 -16
  28. package/consumer-extras/patterns/kpi-flat-band-pattern.md +2 -2
  29. package/dist/components/data-table/index.js +2 -2
  30. package/dist/components/data-table/index.js.map +1 -1
  31. package/dist/components/data-table/pagination.js +3 -3
  32. package/dist/components/data-table/pagination.js.map +1 -1
  33. package/dist/components/data-table/use-table-state.d.ts +1 -1
  34. package/dist/components/data-table/use-table-state.js.map +1 -1
  35. package/dist/components/data-views/data-row-list.js.map +1 -1
  36. package/dist/components/data-views/finder-panel-view.d.ts +1 -1
  37. package/dist/components/data-views/finder-panel-view.js.map +1 -1
  38. package/dist/components/data-views/hub-table.d.ts +9 -3
  39. package/dist/components/data-views/hub-table.js +262 -40
  40. package/dist/components/data-views/hub-table.js.map +1 -1
  41. package/dist/components/data-views/index.js +262 -40
  42. package/dist/components/data-views/index.js.map +1 -1
  43. package/dist/components/data-views/list-page-split-hub-tokens.d.ts +2 -2
  44. package/dist/components/data-views/list-page-split-hub-tokens.js.map +1 -1
  45. package/dist/components/data-views/list-page-tree-column-header.d.ts +1 -1
  46. package/dist/components/data-views/list-page-tree-column-header.js.map +1 -1
  47. package/dist/components/data-views/list-page-tree-panel-shell.js.map +1 -1
  48. package/dist/components/data-views/os-folder-glyph.d.ts +1 -1
  49. package/dist/components/data-views/os-folder-glyph.js.map +1 -1
  50. package/dist/components/ui/avatar.d.ts +1 -1
  51. package/dist/components/ui/banner.d.ts +2 -2
  52. package/dist/components/ui/key-metrics.js.map +1 -1
  53. package/dist/index.js +136 -39
  54. package/dist/index.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/data-table/index.tsx +2 -2
  57. package/src/components/data-table/pagination.tsx +5 -1
  58. package/src/components/data-table/use-table-state.ts +1 -1
  59. package/src/components/data-views/data-row-list.tsx +1 -1
  60. package/src/components/data-views/finder-panel-view.tsx +2 -2
  61. package/src/components/data-views/hub-table.tsx +149 -41
  62. package/src/components/data-views/list-page-split-hub-tokens.ts +2 -2
  63. package/src/components/data-views/list-page-tree-column-header.tsx +1 -1
  64. package/src/components/data-views/os-folder-glyph.tsx +1 -1
  65. package/src/components/ui/key-metrics.tsx +1 -1
  66. package/template/.claude/skills/exxat-ds-skill/SKILL.md +8 -7
  67. package/template/.cursor/rules/exxat-accessibility.mdc +1 -1
  68. package/template/.cursor/rules/exxat-command-menu.mdc +1 -1
  69. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +6 -6
  70. package/template/.cursor/rules/exxat-data-tables.mdc +3 -3
  71. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +5 -5
  72. package/template/.cursor/rules/exxat-mono-ids.mdc +1 -1
  73. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +1 -1
  74. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +1 -1
  75. package/template/AGENTS.md +43 -37
  76. package/template/app/(app)/columns/page.tsx +11 -0
  77. package/template/app/(app)/library/all/page.tsx +11 -0
  78. package/template/app/(app)/library/find/page.tsx +12 -0
  79. package/template/app/(app)/{question-bank → library}/layout.tsx +16 -16
  80. package/template/app/(app)/library/list/page.tsx +12 -0
  81. package/template/app/(app)/{question-bank → library}/new/page.tsx +10 -10
  82. package/template/app/(app)/library/page.tsx +11 -0
  83. package/template/app/(app)/tokens-themes/page.tsx +11 -0
  84. package/template/components/ask-leo-composer.tsx +2 -2
  85. package/template/components/columns-client.tsx +158 -0
  86. package/template/components/columns-showcase.tsx +541 -0
  87. package/template/components/data-views/index.ts +32 -6
  88. package/template/components/data-views/{question-bank-folder-tree-branch.tsx → library-folder-tree-branch.tsx} +19 -19
  89. package/template/components/data-views/table-cells.tsx +673 -0
  90. package/template/components/folder-details-shell.tsx +11 -11
  91. package/template/components/hub-tree-panel-view.tsx +24 -24
  92. package/template/components/{question-bank-board-view.tsx → library-board-view.tsx} +44 -44
  93. package/template/components/{question-bank-client.tsx → library-client.tsx} +82 -82
  94. package/template/components/{question-bank-dashboard-charts.tsx → library-dashboard-charts.tsx} +14 -14
  95. package/template/components/{question-bank-favorite-button.tsx → library-favorite-button.tsx} +7 -7
  96. package/template/components/{question-bank-hub-client.tsx → library-hub-client.tsx} +43 -43
  97. package/template/components/{question-bank-new-folder-sheet.tsx → library-new-folder-sheet.tsx} +14 -14
  98. package/template/components/{question-bank-os-folder-view.tsx → library-os-folder-view.tsx} +31 -31
  99. package/template/components/{question-bank-page-header.tsx → library-page-header.tsx} +6 -6
  100. package/template/components/library-panel-activator.tsx +8 -0
  101. package/template/components/{question-bank-secondary-nav.tsx → library-secondary-nav.tsx} +60 -60
  102. package/template/components/{question-bank-table.tsx → library-table.tsx} +97 -97
  103. package/template/components/list-hub-status-badge.tsx +2 -2
  104. package/template/components/{new-question-composer.tsx → new-library-item-form.tsx} +37 -37
  105. package/template/components/sidebar/app-sidebar.tsx +61 -5
  106. package/template/components/sidebar/secondary-panel.tsx +109 -56
  107. package/template/components/sidebar/sidebar-auto-collapse.tsx +2 -2
  108. package/template/components/sidebar/sidebar-auto-open.tsx +2 -1
  109. package/template/components/table-properties/types.ts +1 -1
  110. package/template/components/templates/discovery-hub-template.tsx +1 -1
  111. package/template/components/templates/new-focus-template.tsx +2 -2
  112. package/template/components/templates/secondary-panel-hub-template.tsx +1 -1
  113. package/template/components/tokens-secondary-nav.tsx +192 -0
  114. package/template/components/tokens-themes-client.tsx +476 -0
  115. package/template/components/tokens-themes-section.tsx +386 -0
  116. package/template/docs/HANDBOOK.md +187 -0
  117. package/template/docs/blueprints/README.md +1 -1
  118. package/template/docs/blueprints/board-card.md +1 -1
  119. package/template/docs/blueprints/data-table.md +2 -2
  120. package/template/docs/blueprints/list-page-template.md +3 -3
  121. package/template/docs/blueprints/page-header.md +4 -4
  122. package/template/docs/collaboration-access-pattern.md +7 -7
  123. package/template/docs/component-selection-guide.md +1 -1
  124. package/template/docs/data-views-pattern.md +18 -16
  125. package/template/docs/glossary.md +58 -0
  126. package/template/docs/kpi-flat-band-pattern.md +3 -3
  127. package/template/docs/kpi-trend-pattern.md +18 -3
  128. package/template/docs/large-dataset-strategy.md +155 -0
  129. package/template/docs/library-hub-header-pattern.md +25 -0
  130. package/template/docs/migrations/_template.md +1 -1
  131. package/template/docs/reference-implementations.md +151 -0
  132. package/template/docs/token-taxonomy.md +1 -1
  133. package/template/docs/voice-and-tone.md +262 -0
  134. package/template/hooks/use-secondary-panel-hub-nav.ts +10 -10
  135. package/template/lib/ask-leo-route-context.ts +6 -18
  136. package/template/lib/coach-mark-registry.ts +0 -16
  137. package/template/lib/command-menu-config.ts +5 -12
  138. package/template/lib/command-menu-search-data.ts +8 -39
  139. package/template/lib/{question-bank-authoring.ts → library-authoring.ts} +89 -88
  140. package/template/lib/library-dedicated-search.ts +19 -0
  141. package/template/lib/library-hub-search.ts +90 -0
  142. package/template/lib/library-nav.ts +477 -0
  143. package/template/lib/library-recent-searches.ts +22 -0
  144. package/template/lib/{placements-supported-views.ts → library-supported-views.ts} +2 -2
  145. package/template/lib/list-status-badges.ts +16 -104
  146. package/template/lib/mock/dashboard.ts +1 -1
  147. package/template/lib/mock/{question-bank-folders.ts → library-folders.ts} +30 -30
  148. package/template/lib/mock/library-header-collaborators.ts +54 -0
  149. package/template/lib/mock/{question-bank-inspector.ts → library-inspector.ts} +29 -29
  150. package/template/lib/mock/{question-bank-kpi.ts → library-kpi.ts} +20 -20
  151. package/template/lib/mock/library.ts +249 -0
  152. package/template/lib/mock/navigation.tsx +32 -26
  153. package/template/lib/table-state-lifecycle.ts +1 -1
  154. package/template/next.config.mjs +7 -4
  155. package/consumer-extras/cursor-rules/exxat-question-bank-hub-header.mdc +0 -28
  156. package/template/app/(app)/examples/page.tsx +0 -41
  157. package/template/app/(app)/question-bank/find/page.tsx +0 -12
  158. package/template/app/(app)/question-bank/library/page.tsx +0 -11
  159. package/template/app/(app)/question-bank/list/page.tsx +0 -12
  160. package/template/app/(app)/question-bank/page.tsx +0 -11
  161. package/template/components/compliance-board-view.tsx +0 -142
  162. package/template/components/compliance-client.tsx +0 -92
  163. package/template/components/compliance-page-header.tsx +0 -89
  164. package/template/components/compliance-table.tsx +0 -468
  165. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  166. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  167. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  168. package/template/components/new-placement-back-btn.tsx +0 -28
  169. package/template/components/new-placement-form.tsx +0 -942
  170. package/template/components/placement-board-card.tsx +0 -250
  171. package/template/components/placement-detail.tsx +0 -438
  172. package/template/components/placements-board-view.tsx +0 -397
  173. package/template/components/placements-client.tsx +0 -220
  174. package/template/components/placements-list-view.tsx +0 -124
  175. package/template/components/placements-page-header.tsx +0 -166
  176. package/template/components/placements-table-cells.test.tsx +0 -22
  177. package/template/components/placements-table-cells.tsx +0 -173
  178. package/template/components/placements-table-columns.tsx +0 -210
  179. package/template/components/placements-table.tsx +0 -934
  180. package/template/components/question-bank-panel-activator.tsx +0 -8
  181. package/template/components/rotations-empty-state.tsx +0 -50
  182. package/template/components/rotations-panel-activator.tsx +0 -8
  183. package/template/components/sites-board-view.tsx +0 -67
  184. package/template/components/sites-client.tsx +0 -154
  185. package/template/components/sites-table.tsx +0 -249
  186. package/template/components/team-board-view.tsx +0 -122
  187. package/template/components/team-client.tsx +0 -100
  188. package/template/components/team-page-header.tsx +0 -92
  189. package/template/components/team-table.tsx +0 -553
  190. package/template/docs/question-bank-hub-header-pattern.md +0 -25
  191. package/template/lib/compliance-supported-views.ts +0 -10
  192. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  193. package/template/lib/mock/compliance-kpi.ts +0 -61
  194. package/template/lib/mock/compliance.ts +0 -146
  195. package/template/lib/mock/placements-kpi.ts +0 -134
  196. package/template/lib/mock/placements.ts +0 -176
  197. package/template/lib/mock/question-bank-header-collaborators.ts +0 -54
  198. package/template/lib/mock/question-bank.ts +0 -249
  199. package/template/lib/mock/sites-directory.ts +0 -16
  200. package/template/lib/mock/sites-kpi.ts +0 -25
  201. package/template/lib/mock/team-kpi.ts +0 -60
  202. package/template/lib/mock/team.ts +0 -118
  203. package/template/lib/placement-board-card-layout.ts +0 -79
  204. package/template/lib/question-bank-dedicated-search.ts +0 -19
  205. package/template/lib/question-bank-hub-search.ts +0 -90
  206. package/template/lib/question-bank-nav.ts +0 -477
  207. package/template/lib/question-bank-recent-searches.ts +0 -22
  208. package/template/lib/question-bank-supported-views.ts +0 -12
  209. package/template/lib/sites-supported-views.ts +0 -10
  210. 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[]