@exxatdesignux/ui 0.2.18 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/consumer-extras/AGENTS.md +76 -0
  3. package/consumer-extras/README.md +5 -1
  4. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +14 -3
  5. package/consumer-extras/cursor-skills/exxat-consumer-app/SKILL.md +37 -0
  6. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +21 -6
  7. package/consumer-extras/cursor-skills/exxat-focused-workflow-page/SKILL.md +57 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +4 -2
  9. package/consumer-extras/patterns/consumer-app-pattern.md +39 -0
  10. package/consumer-extras/patterns/consumer-upgrade-checklist.md +20 -0
  11. package/consumer-extras/patterns/data-views-pattern.md +40 -3
  12. package/consumer-extras/patterns/focused-workflow-page-pattern.md +84 -0
  13. package/consumer-extras/patterns/shell-surface-elevation-pattern.md +5 -3
  14. package/package.json +2 -1
  15. package/src/components/ui/button-group.tsx +81 -0
  16. package/src/components/ui/button.tsx +4 -4
  17. package/src/globals.css +7 -1858
  18. package/src/theme.css +10 -1126
  19. package/src/tokens/README.md +15 -0
  20. package/src/tokens/base.css +337 -0
  21. package/src/tokens/high-contrast.css +1195 -0
  22. package/src/tokens/layers.css +224 -0
  23. package/src/tokens/tailwind-bridge.css +118 -0
  24. package/src/tokens/themes.css +201 -0
  25. package/template/AGENTS.md +60 -22
  26. package/template/app/(app)/dashboard/loading.tsx +3 -15
  27. package/template/app/(app)/dashboard/page.tsx +2 -14
  28. package/template/app/(app)/data-list/layout.tsx +43 -0
  29. package/template/app/(app)/data-list/page.tsx +2 -2
  30. package/template/app/(app)/examples/focused-workflow/page.tsx +5 -0
  31. package/template/app/(app)/examples/page.tsx +1 -0
  32. package/template/app/(app)/loading.tsx +1 -18
  33. package/template/app/(app)/question-bank/find/page.tsx +2 -1
  34. package/template/app/(app)/question-bank/library/page.tsx +2 -1
  35. package/template/app/(app)/question-bank/list/page.tsx +2 -1
  36. package/template/app/(app)/question-bank/new/page.tsx +15 -23
  37. package/template/app/(app)/question-bank/page.tsx +2 -1
  38. package/template/app/(app)/settings/page.tsx +4 -5
  39. package/template/app/globals.css +7 -1964
  40. package/template/components/app-route-loading.tsx +14 -0
  41. package/template/components/app-sidebar.tsx +70 -55
  42. package/template/components/data-views/index.ts +37 -9
  43. package/template/components/data-views/list-page-calendar-view.tsx +593 -0
  44. package/template/components/data-views/list-page-connected-view-body.tsx +66 -0
  45. package/template/components/data-views/list-page-folder-columns-panel.tsx +345 -0
  46. package/template/components/data-views/list-page-split-hub-chrome.tsx +8 -0
  47. package/template/components/examples/focused-workflow-showcase.tsx +183 -0
  48. package/template/components/list-hub-board-view.tsx +68 -0
  49. package/template/components/list-hub-client.tsx +186 -0
  50. package/template/components/list-hub-list-view.tsx +36 -0
  51. package/template/components/list-hub-panel-activator.tsx +8 -0
  52. package/template/components/list-hub-secondary-nav.tsx +121 -0
  53. package/template/components/list-hub-table.tsx +336 -0
  54. package/template/components/new-question-composer.tsx +6 -24
  55. package/template/components/product-switcher.tsx +3 -2
  56. package/template/components/question-bank-client.tsx +4 -1
  57. package/template/components/question-bank-folder-columns-panel.tsx +104 -0
  58. package/template/components/question-bank-table.tsx +143 -485
  59. package/template/components/secondary-panel/nav-link-rows.tsx +83 -0
  60. package/template/components/secondary-panel.tsx +4 -44
  61. package/template/components/secondary-panels/list-hub-panel.tsx +39 -0
  62. package/template/components/secondary-panels/question-bank-panel.tsx +39 -0
  63. package/template/components/secondary-panels/registry.tsx +15 -0
  64. package/template/components/settings-appearance-card.tsx +3 -2
  65. package/template/components/settings-client.tsx +59 -15
  66. package/template/components/settings-form-row.tsx +9 -4
  67. package/template/components/table-properties/drawer-button.tsx +13 -0
  68. package/template/components/table-properties/drawer.tsx +65 -4
  69. package/template/components/templates/focused-workflow-layouts.tsx +448 -0
  70. package/template/components/templates/focused-workflow-page-template.tsx +69 -0
  71. package/template/components/templates/list-page.tsx +29 -5
  72. package/template/components/templates/nested-secondary-panel-shell.tsx +2 -1
  73. package/template/components/templates/page-loading-shell.tsx +262 -0
  74. package/template/components/ui/button-group.tsx +1 -0
  75. package/template/docs/consumer-app-pattern.md +39 -0
  76. package/template/docs/data-views-pattern.md +40 -3
  77. package/template/docs/drawer-vs-dialog-pattern.md +3 -1
  78. package/template/docs/focused-workflow-page-pattern.md +84 -0
  79. package/template/docs/shell-surface-elevation-pattern.md +5 -3
  80. package/template/lib/command-menu-search-data.ts +11 -27
  81. package/template/lib/data-list-display-options.ts +16 -2
  82. package/template/lib/data-list-view-registry.ts +104 -0
  83. package/template/lib/data-list-view-surface.ts +15 -1
  84. package/template/lib/data-list-view.ts +10 -1
  85. package/template/lib/data-view-dashboard-storage.ts +38 -35
  86. package/template/lib/hub-connected-view-renderers.ts +58 -0
  87. package/template/lib/list-hub-nav.ts +121 -0
  88. package/template/lib/list-hub-supported-views.ts +10 -0
  89. package/template/lib/list-page-table-properties.ts +3 -7
  90. package/template/lib/list-status-badges.ts +4 -97
  91. package/template/lib/mock/list-hub-directory.ts +27 -0
  92. package/template/lib/mock/list-hub-kpi.ts +27 -0
  93. package/template/lib/mock/navigation.tsx +1 -0
  94. package/template/lib/page-loading-variant.ts +40 -0
  95. package/template/lib/question-bank-supported-views.ts +13 -0
  96. package/template/lib/table-state-lifecycle.ts +2 -2
  97. package/template/app/(app)/data-list/[id]/page.tsx +0 -44
  98. package/template/app/(app)/data-list/new/page.tsx +0 -34
  99. package/template/components/compliance-board-view.tsx +0 -142
  100. package/template/components/compliance-client.tsx +0 -92
  101. package/template/components/compliance-list-view.tsx +0 -54
  102. package/template/components/compliance-page-header.tsx +0 -89
  103. package/template/components/compliance-table.tsx +0 -612
  104. package/template/components/data-view-dashboard-charts-compliance.tsx +0 -963
  105. package/template/components/data-view-dashboard-charts-team.tsx +0 -971
  106. package/template/components/data-view-dashboard-charts.tsx +0 -1503
  107. package/template/components/new-placement-back-btn.tsx +0 -28
  108. package/template/components/new-placement-form.tsx +0 -1068
  109. package/template/components/placement-board-card.tsx +0 -262
  110. package/template/components/placement-detail.tsx +0 -438
  111. package/template/components/placements-board-view.tsx +0 -404
  112. package/template/components/placements-client.tsx +0 -252
  113. package/template/components/placements-list-view.tsx +0 -171
  114. package/template/components/placements-page-header.tsx +0 -166
  115. package/template/components/placements-table-cells.test.tsx +0 -22
  116. package/template/components/placements-table-cells.tsx +0 -173
  117. package/template/components/placements-table-columns.tsx +0 -640
  118. package/template/components/placements-table.tsx +0 -1642
  119. package/template/components/rotations-empty-state.tsx +0 -50
  120. package/template/components/rotations-panel-activator.tsx +0 -8
  121. package/template/components/sites-all-client.tsx +0 -154
  122. package/template/components/sites-board-view.tsx +0 -67
  123. package/template/components/sites-list-view.tsx +0 -42
  124. package/template/components/sites-table.tsx +0 -382
  125. package/template/components/team-board-view.tsx +0 -122
  126. package/template/components/team-client.tsx +0 -100
  127. package/template/components/team-list-view.tsx +0 -59
  128. package/template/components/team-page-header.tsx +0 -92
  129. package/template/components/team-table.tsx +0 -693
  130. package/template/lib/data-view-dashboard-placements-layout.ts +0 -215
  131. package/template/lib/mock/compliance-kpi.ts +0 -61
  132. package/template/lib/mock/compliance.ts +0 -146
  133. package/template/lib/mock/placements-kpi.ts +0 -134
  134. package/template/lib/mock/placements.ts +0 -183
  135. package/template/lib/mock/sites-directory.ts +0 -16
  136. package/template/lib/mock/sites-kpi.ts +0 -25
  137. package/template/lib/mock/team-kpi.ts +0 -60
  138. package/template/lib/mock/team.ts +0 -118
  139. package/template/lib/placement-board-card-layout.ts +0 -79
  140. package/template/lib/placement-lifecycle.ts +0 -5
@@ -1,171 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * PlacementsListView — full-width row layout for the data list (vs table grid / board columns).
5
- * Shares column visibility + lifecycle rules with Table Properties via the same board column model.
6
- *
7
- * Shell (empty state + virtualization above 80 rows) comes from the generic
8
- * `DataRowList` primitive in `components/data-views/`. This file owns only
9
- * the placement-specific row body (column-driven field visibility).
10
- */
11
-
12
- import * as React from "react"
13
- import { useRouter } from "next/navigation"
14
- import type { Placement } from "@/lib/mock/placements"
15
- import { StatusBadge } from "@/components/placements-table-cells"
16
- import { Badge } from "@/components/ui/badge"
17
- import { ListPageBoardCard, ListPageBoardCardAvatar } from "@/components/data-views/list-page-board-card"
18
- import { DataRowList } from "@/components/data-views/data-row-list"
19
- import {
20
- type BoardCardLifecycleTabId,
21
- isBoardFieldActive,
22
- scheduleKeysForTab,
23
- } from "@/lib/placement-board-card-layout"
24
- import { getConditionalRowBackground } from "@/lib/conditional-rule-match"
25
- import type { ConditionalRule } from "@/components/table-properties/types"
26
- import type { ColumnDef } from "@/components/data-table/types"
27
-
28
- /** Above this count, `DataRowList` virtualizes against the window scroll. */
29
- const VIRTUAL_ROWS_THRESHOLD = 80
30
- /** Initial row height guess (px); `measureElement` refines for variable content. */
31
- const ESTIMATE_ROW_PX = 100
32
-
33
- function scheduleSummary(row: Placement, tab: BoardCardLifecycleTabId): string | null {
34
- switch (tab) {
35
- case "all":
36
- return [row.start, row.duration].filter(Boolean).join(" · ") || null
37
- case "upcoming":
38
- return row.daysUntilStart > 0
39
- ? `${row.start} · Starts in ${row.daysUntilStart} days`
40
- : row.start
41
- case "ongoing":
42
- return `${row.progressWeeksDone} / ${row.progressWeeksTotal} wks · Ends ${row.endDate}`
43
- case "completed":
44
- return [row.completionDate, row.finalStatus].filter(v => v && v !== "—").join(" · ") || null
45
- default:
46
- return null
47
- }
48
- }
49
-
50
- function PlacementListRowContent({
51
- row,
52
- tab,
53
- hiddenColKeys,
54
- boardColumns,
55
- conditionalRules,
56
- onOpen,
57
- }: {
58
- row: Placement
59
- tab: BoardCardLifecycleTabId
60
- hiddenColKeys: Set<string>
61
- boardColumns: ColumnDef<Placement>[]
62
- conditionalRules: ConditionalRule[] | undefined
63
- onOpen: (id: number) => void
64
- }) {
65
- const ruleBg = getConditionalRowBackground(row, conditionalRules, boardColumns)
66
- const showStudent = isBoardFieldActive("student", tab, hiddenColKeys, boardColumns)
67
- const showStatus = isBoardFieldActive("status", tab, hiddenColKeys, boardColumns)
68
- const showSite = isBoardFieldActive("site", tab, hiddenColKeys, boardColumns)
69
- const showSpec = isBoardFieldActive("specialization", tab, hiddenColKeys, boardColumns)
70
- const showInternship = isBoardFieldActive("internship", tab, hiddenColKeys, boardColumns)
71
- const sk = scheduleKeysForTab(tab)
72
- const showSchedule = sk.some(k => isBoardFieldActive(k, tab, hiddenColKeys, boardColumns))
73
- const schedule = showSchedule ? scheduleSummary(row, tab) : null
74
-
75
- const title = showStudent ? row.student : `Placement ${row.id}`
76
-
77
- const leading = showStudent ? (
78
- <ListPageBoardCardAvatar initials={row.initials} className="size-9" />
79
- ) : (
80
- <span className="size-9 shrink-0 rounded-full bg-muted/80" aria-hidden />
81
- )
82
-
83
- const rowEnd = showStatus ? (
84
- <div className="flex shrink-0 items-center gap-2 pt-0.5">
85
- <StatusBadge status={row.status} surface="board" />
86
- <i className="fa-light fa-chevron-right text-xs text-muted-foreground" aria-hidden />
87
- </div>
88
- ) : (
89
- <i className="fa-light fa-chevron-right mt-1 shrink-0 text-xs text-muted-foreground" aria-hidden />
90
- )
91
-
92
- return (
93
- <ListPageBoardCard
94
- layout="row"
95
- leading={leading}
96
- rowEnd={rowEnd}
97
- isNew={row.isNew}
98
- style={ruleBg ? { background: ruleBg } : undefined}
99
- onClick={() => onOpen(row.id)}
100
- >
101
- <div className="space-y-1">
102
- <div className="flex flex-wrap items-center gap-2">
103
- <span className="text-sm font-semibold text-foreground">{title}</span>
104
- {row.isNew ? (
105
- <Badge variant="secondary" className="h-5 px-1.5 text-xs font-medium">
106
- New
107
- </Badge>
108
- ) : null}
109
- </div>
110
- {showSite ? (
111
- <p className="text-xs text-foreground/90">
112
- <span className="font-medium">{row.site}</span>
113
- {row.siteAddress ? (
114
- <span className="text-muted-foreground"> · {row.siteAddress}</span>
115
- ) : null}
116
- </p>
117
- ) : null}
118
- {(showSpec || showInternship) ? (
119
- <p className="text-xs text-muted-foreground">
120
- {[showSpec ? row.specialization : null, showInternship ? row.internship : null]
121
- .filter(Boolean)
122
- .join(" · ")}
123
- </p>
124
- ) : null}
125
- {schedule ? <p className="text-xs text-muted-foreground tabular-nums">{schedule}</p> : null}
126
- </div>
127
- </ListPageBoardCard>
128
- )
129
- }
130
-
131
- export interface PlacementsListViewProps {
132
- rows: Placement[]
133
- lifecycleTabId: BoardCardLifecycleTabId
134
- hiddenColKeys: Set<string>
135
- boardColumns: ColumnDef<Placement>[]
136
- conditionalRules?: ConditionalRule[]
137
- emptyCopy: string
138
- }
139
-
140
- export function PlacementsListView({
141
- rows,
142
- lifecycleTabId,
143
- hiddenColKeys,
144
- boardColumns,
145
- conditionalRules,
146
- emptyCopy,
147
- }: PlacementsListViewProps) {
148
- const router = useRouter()
149
- const onOpen = React.useCallback((id: number) => router.push(`/data-list/${id}`), [router])
150
-
151
- return (
152
- <DataRowList<Placement>
153
- rows={rows}
154
- getRowId={row => row.id}
155
- emptyState={emptyCopy}
156
- ariaLabel="Placements"
157
- virtualizeThreshold={VIRTUAL_ROWS_THRESHOLD}
158
- estimatedRowHeight={ESTIMATE_ROW_PX}
159
- renderRow={row => (
160
- <PlacementListRowContent
161
- row={row}
162
- tab={lifecycleTabId}
163
- hiddenColKeys={hiddenColKeys}
164
- boardColumns={boardColumns}
165
- conditionalRules={conditionalRules}
166
- onOpen={onOpen}
167
- />
168
- )}
169
- />
170
- )
171
- }
@@ -1,166 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { Button } from "@/components/ui/button"
5
- import { Kbd, KbdGroup } from "@/components/ui/kbd"
6
- import { PageHeader } from "@/components/page-header"
7
- import {
8
- DropdownMenu,
9
- DropdownMenuContent,
10
- DropdownMenuItem,
11
- DropdownMenuSeparator,
12
- DropdownMenuTrigger,
13
- Shortcut,
14
- } from "@/components/ui/dropdown-menu"
15
- import { Tip } from "@/components/ui/tip"
16
- import { useAltKeyLabel, useModKeyLabel } from "@/hooks/use-mod-key-label"
17
- import { isEditableTarget } from "@/lib/editable-target"
18
-
19
- export interface PlacementsPageHeaderProps {
20
- /** Main heading in the page header */
21
- title?: string
22
- /** Primary button label */
23
- primaryCtaLabel?: string
24
- /** Shown under the page title */
25
- subtitle?: string
26
- onNewPlacement: () => void
27
- onExport: () => void
28
- showMetrics: boolean
29
- onToggleMetrics: () => void
30
- /** When false, title + subtitle are hidden visually (Display options). */
31
- showTitleBlock?: boolean
32
- className?: string
33
- }
34
-
35
- /**
36
- * List hub shell header — title, primary CTA, overflow menu (export, metrics).
37
- * Reusable for any route that needs the same chrome.
38
- */
39
- export function PlacementsPageHeader({
40
- title = "Sample records",
41
- primaryCtaLabel = "New row",
42
- subtitle = "24 demo rows · Last updated now",
43
- onNewPlacement,
44
- onExport,
45
- showMetrics,
46
- onToggleMetrics,
47
- showTitleBlock = true,
48
- className,
49
- }: PlacementsPageHeaderProps) {
50
- const mod = useModKeyLabel()
51
- const alt = useAltKeyLabel()
52
- const [moreOpen, setMoreOpen] = React.useState(false)
53
-
54
- /** ⌘⌥N / Ctrl+Alt+N — avoids ⌘⇧N / Ctrl+Shift+N (private/incognito windows). */
55
- React.useEffect(() => {
56
- function onKey(e: KeyboardEvent) {
57
- if (!e.altKey || (!e.metaKey && !e.ctrlKey)) return
58
- if (e.key.toLowerCase() !== "n") return
59
- if (isEditableTarget(e.target)) return
60
- e.preventDefault()
61
- onNewPlacement()
62
- }
63
- document.addEventListener("keydown", onKey)
64
- return () => document.removeEventListener("keydown", onKey)
65
- }, [onNewPlacement])
66
-
67
- /** ⌘⌥M / Ctrl+Alt+M — avoids ⌘⇧O / Ctrl+Shift+O (bookmark manager in Chromium). */
68
- React.useEffect(() => {
69
- function onKey(e: KeyboardEvent) {
70
- if (!e.altKey || (!e.metaKey && !e.ctrlKey)) return
71
- if (e.key.toLowerCase() !== "m") return
72
- if (isEditableTarget(e.target)) return
73
- e.preventDefault()
74
- setMoreOpen(o => !o)
75
- }
76
- document.addEventListener("keydown", onKey)
77
- return () => document.removeEventListener("keydown", onKey)
78
- }, [])
79
-
80
- return (
81
- <>
82
- <Shortcut keys="⌘⇧E" onInvoke={onExport} />
83
- <Shortcut keys="⌘⌥H" onInvoke={onToggleMetrics} />
84
- <PageHeader
85
- title={title}
86
- subtitle={subtitle}
87
- className={className}
88
- showTitleBlock={showTitleBlock}
89
- actions={
90
- <div className="flex items-center gap-2" role="group" aria-label="Primary list actions">
91
- <Tip
92
- side="bottom"
93
- label={
94
- <>
95
- <span>{primaryCtaLabel}</span>
96
- <KbdGroup>
97
- <Kbd>{mod}</Kbd>
98
- <Kbd>{alt}</Kbd>
99
- <Kbd>N</Kbd>
100
- </KbdGroup>
101
- </>
102
- }
103
- >
104
- <Button size="lg" onClick={onNewPlacement}>
105
- <i className="fa-light fa-plus" aria-hidden="true" />
106
- {primaryCtaLabel}
107
- </Button>
108
- </Tip>
109
- <DropdownMenu open={moreOpen} onOpenChange={setMoreOpen}>
110
- <Tip
111
- side="bottom"
112
- label={
113
- <>
114
- <span>More actions</span>
115
- <KbdGroup>
116
- <Kbd>{mod}</Kbd>
117
- <Kbd>{alt}</Kbd>
118
- <Kbd>M</Kbd>
119
- </KbdGroup>
120
- </>
121
- }
122
- >
123
- <DropdownMenuTrigger asChild>
124
- <Button
125
- size="lg"
126
- variant="outline"
127
- className="aspect-square px-0"
128
- aria-label="More actions"
129
- >
130
- <i className="fa-light fa-ellipsis text-base" aria-hidden="true" />
131
- </Button>
132
- </DropdownMenuTrigger>
133
- </Tip>
134
- <DropdownMenuContent align="end">
135
- <DropdownMenuItem
136
- shortcut="⌘⇧E"
137
- onSelect={() => {
138
- /* Defer past Radix menu close + focus restore so Export Sheet mounts reliably. */
139
- window.setTimeout(() => onExport(), 0)
140
- }}
141
- >
142
- <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
143
- Export
144
- </DropdownMenuItem>
145
- <DropdownMenuSeparator />
146
- <DropdownMenuItem
147
- shortcut="⌘⌥H"
148
- onSelect={() => {
149
- window.setTimeout(() => onToggleMetrics(), 0)
150
- }}
151
- >
152
- <i
153
- className={`fa-light ${showMetrics ? "fa-eye-slash" : "fa-eye"}`}
154
- aria-hidden="true"
155
- />
156
- {showMetrics ? "Hide metric section" : "Show metric section"}
157
- </DropdownMenuItem>
158
- </DropdownMenuContent>
159
- </DropdownMenu>
160
- </div>
161
- }
162
- />
163
- </>
164
- )
165
- }
166
-
@@ -1,22 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
- import { render, screen } from "@testing-library/react"
3
-
4
- import { HireBadge, ReadinessBadge, StatusBadge } from "./placements-table-cells"
5
-
6
- describe("placements-table-cells", () => {
7
- it("renders StatusBadge label for confirmed", () => {
8
- render(<StatusBadge status="confirmed" />)
9
- expect(screen.getByText("Confirmed")).toBeInTheDocument()
10
- })
11
-
12
- it("ReadinessBadge uses destructive variant for risk copy", () => {
13
- const { container } = render(<ReadinessBadge value="At risk" />)
14
- expect(container.querySelector("[data-slot='badge']")).toBeTruthy()
15
- expect(screen.getByText("At risk")).toBeInTheDocument()
16
- })
17
-
18
- it("HireBadge shows em dash for empty", () => {
19
- render(<HireBadge value="" />)
20
- expect(screen.getByText("—")).toBeInTheDocument()
21
- })
22
- })
@@ -1,173 +0,0 @@
1
- "use client"
2
-
3
- /**
4
- * Placement table cell primitives — extracted from placements-table for reuse and easier testing.
5
- */
6
-
7
- import * as React from "react"
8
- import { Badge } from "@/components/ui/badge"
9
- import { Button } from "@/components/ui/button"
10
- import { Tip } from "@/components/ui/tip"
11
- import {
12
- DropdownMenu,
13
- DropdownMenuContent,
14
- DropdownMenuItem,
15
- DropdownMenuSeparator,
16
- DropdownMenuTrigger,
17
- } from "@/components/ui/dropdown-menu"
18
- import { ListHubStatusBadge } from "@/components/list-hub-status-badge"
19
- import {
20
- PLACEMENT_STATUS_BADGE_CLASS,
21
- PLACEMENT_STATUS_ICON,
22
- PLACEMENT_STATUS_LABEL,
23
- } from "@/lib/list-status-badges"
24
- import { AvatarInitials } from "@/components/ui/avatar"
25
- import type { Placement, Status } from "@/lib/mock/placements"
26
-
27
- // ─────────────────────────────────────────────────────────────────────────────
28
- // Placement status — same maps + shell as other list hubs (`list-status-badges`)
29
- // ─────────────────────────────────────────────────────────────────────────────
30
-
31
- function isPlacementStatus(v: string): v is Status {
32
- return v in PLACEMENT_STATUS_LABEL
33
- }
34
-
35
- export function StatusBadge({
36
- status,
37
- surface = "table",
38
- }: {
39
- status: Status | string
40
- surface?: "table" | "board"
41
- }) {
42
- if (!isPlacementStatus(status)) {
43
- return (
44
- <Badge variant="outline" className="text-xs shrink-0">
45
- {String(status)}
46
- </Badge>
47
- )
48
- }
49
- return (
50
- <ListHubStatusBadge
51
- surface={surface}
52
- label={PLACEMENT_STATUS_LABEL[status]}
53
- tintClassName={PLACEMENT_STATUS_BADGE_CLASS[status]}
54
- icon={PLACEMENT_STATUS_ICON[status]}
55
- />
56
- )
57
- }
58
-
59
- export function AvatarCircle({ initials }: { initials: string }) {
60
- return (
61
- <AvatarInitials
62
- initials={initials}
63
- className="size-7 shrink-0 text-xs"
64
- fallbackClassName="text-xs"
65
- />
66
- )
67
- }
68
-
69
- export function WeeksProgressCell({ row }: { row: Placement }) {
70
- const { progressWeeksDone, progressWeeksTotal } = row
71
- const total = Math.max(1, progressWeeksTotal)
72
- const pct = Math.min(100, Math.round((progressWeeksDone / total) * 100))
73
- return (
74
- <div className="flex min-w-[128px] max-w-[200px] flex-col gap-1.5">
75
- <div className="h-2 overflow-hidden rounded-full bg-muted">
76
- <div
77
- className="h-full rounded-full bg-primary transition-[width]"
78
- style={{ width: `${pct}%` }}
79
- />
80
- </div>
81
- <span className="text-xs tabular-nums text-muted-foreground">
82
- {progressWeeksDone} / {progressWeeksTotal} wks
83
- </span>
84
- </div>
85
- )
86
- }
87
-
88
- export function ReadinessBadge({ value }: { value: string }) {
89
- const lower = value.toLowerCase()
90
- const variant =
91
- lower.includes("risk") || lower.includes("blocked")
92
- ? "destructive"
93
- : lower.includes("review")
94
- ? "secondary"
95
- : "outline"
96
- return (
97
- <Badge variant={variant} className="h-6 px-2 py-1 text-xs font-medium leading-none">
98
- {value}
99
- </Badge>
100
- )
101
- }
102
-
103
- export function HireBadge({ value }: { value: string }) {
104
- if (value === "—" || !value) return <span className="text-sm text-muted-foreground">—</span>
105
- const yes = value.toLowerCase() === "yes"
106
- return (
107
- <Badge
108
- variant={yes ? "default" : "secondary"}
109
- className="h-6 border-0 px-2 py-1 text-xs font-medium leading-none"
110
- >
111
- {value}
112
- </Badge>
113
- )
114
- }
115
-
116
- // ─────────────────────────────────────────────────────────────────────────────
117
- // Row actions
118
- // ─────────────────────────────────────────────────────────────────────────────
119
-
120
- export interface RowActionDef {
121
- label: string
122
- icon: string
123
- onClick: (row: Placement) => void
124
- variant?: "destructive"
125
- }
126
-
127
- export const PLACEMENT_ROW_ACTIONS: RowActionDef[] = [
128
- { label: "Edit", icon: "fa-pen-to-square", onClick: _row => {} },
129
- { label: "Open", icon: "fa-arrow-up-right", onClick: _row => {} },
130
- { label: "Delete", icon: "fa-trash", onClick: _row => {}, variant: "destructive" },
131
- ]
132
-
133
- export function RowActions({ row, actions }: { row: Placement; actions: RowActionDef[] }) {
134
- if (!actions.length) return null
135
-
136
- if (actions.length === 1) {
137
- const a = actions[0]
138
- return (
139
- <Tip label={a.label} side="top">
140
- <Button size="icon-sm" variant="ghost" aria-label={`${a.label} ${row.student}`}
141
- onClick={() => a.onClick(row)}>
142
- <i className={`fa-light ${a.icon} text-sm`} aria-hidden="true" />
143
- </Button>
144
- </Tip>
145
- )
146
- }
147
-
148
- return (
149
- <DropdownMenu>
150
- <Tip label={`More options for ${row.student}`} side="top">
151
- <DropdownMenuTrigger asChild>
152
- <Button size="icon-sm" variant="ghost" aria-label={`More options for ${row.student}`}>
153
- <i className="fa-light fa-ellipsis text-sm" aria-hidden="true" />
154
- </Button>
155
- </DropdownMenuTrigger>
156
- </Tip>
157
- <DropdownMenuContent align="end">
158
- {actions.map((a, i) => (
159
- <React.Fragment key={a.label}>
160
- {a.variant === "destructive" && i > 0 && <DropdownMenuSeparator />}
161
- <DropdownMenuItem
162
- onClick={() => a.onClick(row)}
163
- className={a.variant === "destructive" ? "text-destructive focus:text-destructive" : ""}
164
- >
165
- <i className={`fa-light ${a.icon}`} aria-hidden="true" />
166
- {a.label}
167
- </DropdownMenuItem>
168
- </React.Fragment>
169
- ))}
170
- </DropdownMenuContent>
171
- </DropdownMenu>
172
- )
173
- }