@exxatdesignux/ui 0.0.6 → 0.0.8

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 (264) hide show
  1. package/bin/init.mjs +29 -0
  2. package/package.json +7 -2
  3. package/template/.nvmrc +1 -0
  4. package/template/.prettierignore +7 -0
  5. package/template/.prettierrc +11 -0
  6. package/template/AGENTS.md +485 -0
  7. package/template/Logo/Exxat_Prism.svg +39 -0
  8. package/template/Logo/Exxat_one.svg +36 -0
  9. package/template/README.md +58 -0
  10. package/template/app/(app)/compliance/page.tsx +10 -0
  11. package/template/app/(app)/dashboard/loading.tsx +18 -0
  12. package/template/app/(app)/dashboard/page.tsx +36 -0
  13. package/template/app/(app)/data-list/[id]/page.tsx +28 -0
  14. package/template/app/(app)/data-list/new/page.tsx +31 -0
  15. package/template/app/(app)/data-list/page.tsx +10 -0
  16. package/template/app/(app)/error.tsx +43 -0
  17. package/template/app/(app)/help/page.tsx +34 -0
  18. package/template/app/(app)/layout.tsx +54 -0
  19. package/template/app/(app)/loading.tsx +18 -0
  20. package/template/app/(app)/question-bank/page.tsx +10 -0
  21. package/template/app/(app)/rotations/page.tsx +15 -0
  22. package/template/app/(app)/settings/page.tsx +17 -0
  23. package/template/app/(app)/sites/all/page.tsx +13 -0
  24. package/template/app/(app)/team/page.tsx +10 -0
  25. package/template/app/favicon.ico +0 -0
  26. package/template/app/globals.css +1811 -0
  27. package/template/app/layout.tsx +95 -0
  28. package/template/app/page.tsx +9 -0
  29. package/template/components/.gitkeep +0 -0
  30. package/template/components/app-sidebar-dynamic.tsx +15 -0
  31. package/template/components/app-sidebar.tsx +901 -0
  32. package/template/components/ask-leo-composer.tsx +216 -0
  33. package/template/components/ask-leo-sidebar.tsx +509 -0
  34. package/template/components/chart-area-interactive.tsx +293 -0
  35. package/template/components/charts-overview.tsx +2321 -0
  36. package/template/components/command-menu-01.tsx +133 -0
  37. package/template/components/command-menu-02.tsx +386 -0
  38. package/template/components/command-menu.tsx +182 -0
  39. package/template/components/compliance-board-view.tsx +134 -0
  40. package/template/components/compliance-client.tsx +92 -0
  41. package/template/components/compliance-list-view.tsx +59 -0
  42. package/template/components/compliance-page-header.tsx +89 -0
  43. package/template/components/compliance-table.tsx +525 -0
  44. package/template/components/dashboard-onboarding-gallery.tsx +13 -0
  45. package/template/components/dashboard-onboarding.tsx +21 -0
  46. package/template/components/dashboard-promo-banner.tsx +67 -0
  47. package/template/components/dashboard-quota-progress-card.tsx +369 -0
  48. package/template/components/dashboard-report-charts.tsx +69 -0
  49. package/template/components/dashboard-section-heading.tsx +68 -0
  50. package/template/components/dashboard-tabs.tsx +598 -0
  51. package/template/components/data-list-client.tsx +239 -0
  52. package/template/components/data-list-table-cells.test.tsx +22 -0
  53. package/template/components/data-list-table-cells.tsx +173 -0
  54. package/template/components/data-list-table.tsx +879 -0
  55. package/template/components/data-table/filter-date-calendar.tsx +38 -0
  56. package/template/components/data-table/filter-text-value-input.tsx +77 -0
  57. package/template/components/data-table/index.tsx +1612 -0
  58. package/template/components/data-table/pagination.tsx +256 -0
  59. package/template/components/data-table/types.ts +91 -0
  60. package/template/components/data-table/use-table-state.ts +566 -0
  61. package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
  62. package/template/components/data-view-dashboard-charts-team.tsx +968 -0
  63. package/template/components/data-view-dashboard-charts.tsx +1668 -0
  64. package/template/components/data-views/board-card-primitives.tsx +93 -0
  65. package/template/components/data-views/index.ts +41 -0
  66. package/template/components/data-views/list-page-board-card.tsx +192 -0
  67. package/template/components/data-views/list-page-board-template.tsx +122 -0
  68. package/template/components/data-views/placement-board-card.tsx +262 -0
  69. package/template/components/export-drawer.tsx +375 -0
  70. package/template/components/exxat-product-logo.tsx +453 -0
  71. package/template/components/form-layout-01.tsx +131 -0
  72. package/template/components/getting-started.tsx +625 -0
  73. package/template/components/key-metrics.tsx +920 -0
  74. package/template/components/leo-insight-indicator.tsx +364 -0
  75. package/template/components/leo-typing-dots.tsx +121 -0
  76. package/template/components/list-hub-status-badge.tsx +51 -0
  77. package/template/components/list-page-dashboard-charts.tsx +18 -0
  78. package/template/components/nav-documents.tsx +89 -0
  79. package/template/components/nav-main.tsx +58 -0
  80. package/template/components/nav-secondary.tsx +64 -0
  81. package/template/components/nav-user.tsx +190 -0
  82. package/template/components/new-placement-back-btn.tsx +28 -0
  83. package/template/components/new-placement-form.tsx +1066 -0
  84. package/template/components/onboarding/index.ts +4 -0
  85. package/template/components/onboarding/onboarding-01.tsx +7 -0
  86. package/template/components/onboarding/onboarding-02.tsx +7 -0
  87. package/template/components/onboarding/onboarding-03.tsx +7 -0
  88. package/template/components/onboarding/onboarding-04.tsx +7 -0
  89. package/template/components/page-header.tsx +57 -0
  90. package/template/components/placement-detail.tsx +438 -0
  91. package/template/components/placements-board-view.tsx +404 -0
  92. package/template/components/placements-list-view.tsx +285 -0
  93. package/template/components/placements-page-header.tsx +160 -0
  94. package/template/components/placements-table-columns.tsx +639 -0
  95. package/template/components/product-switcher.tsx +116 -0
  96. package/template/components/question-bank-board-view.tsx +205 -0
  97. package/template/components/question-bank-client.tsx +77 -0
  98. package/template/components/question-bank-list-view.tsx +59 -0
  99. package/template/components/question-bank-page-header.tsx +89 -0
  100. package/template/components/question-bank-table.tsx +586 -0
  101. package/template/components/rotations-empty-state.tsx +47 -0
  102. package/template/components/rotations-panel-activator.tsx +8 -0
  103. package/template/components/secondary-nav.tsx +394 -0
  104. package/template/components/secondary-panel.tsx +239 -0
  105. package/template/components/section-cards.tsx +106 -0
  106. package/template/components/settings-appearance-card.tsx +424 -0
  107. package/template/components/settings-client.tsx +537 -0
  108. package/template/components/settings-form-row.tsx +42 -0
  109. package/template/components/sidebar-auto-collapse.tsx +23 -0
  110. package/template/components/sidebar-auto-open.tsx +18 -0
  111. package/template/components/sidebar-shell.tsx +37 -0
  112. package/template/components/site-header.tsx +93 -0
  113. package/template/components/sites-all-client.tsx +154 -0
  114. package/template/components/sites-board-view.tsx +67 -0
  115. package/template/components/sites-list-view.tsx +47 -0
  116. package/template/components/sites-table.tsx +312 -0
  117. package/template/components/system-banner-slot.tsx +66 -0
  118. package/template/components/table-properties/column-row.tsx +90 -0
  119. package/template/components/table-properties/draggable-list.ts +49 -0
  120. package/template/components/table-properties/drawer-button.tsx +231 -0
  121. package/template/components/table-properties/drawer.tsx +1102 -0
  122. package/template/components/table-properties/filter-card.tsx +251 -0
  123. package/template/components/table-properties/index.ts +22 -0
  124. package/template/components/table-properties/sort-card.tsx +59 -0
  125. package/template/components/table-properties/types.ts +124 -0
  126. package/template/components/task-list-panel.tsx +98 -0
  127. package/template/components/task-priority-badge.tsx +28 -0
  128. package/template/components/team-board-view.tsx +114 -0
  129. package/template/components/team-client.tsx +93 -0
  130. package/template/components/team-list-view.tsx +62 -0
  131. package/template/components/team-page-header.tsx +92 -0
  132. package/template/components/team-table.tsx +525 -0
  133. package/template/components/templates/list-page.tsx +576 -0
  134. package/template/components/templates/primary-page-template.tsx +56 -0
  135. package/template/components/theme-color-sync.tsx +32 -0
  136. package/template/components/theme-provider.tsx +71 -0
  137. package/template/components/tinted-icon-disc.tsx +53 -0
  138. package/template/components/ui/ai-thinking-surface.tsx +121 -0
  139. package/template/components/ui/avatar.tsx +1 -0
  140. package/template/components/ui/badge.tsx +1 -0
  141. package/template/components/ui/banner.tsx +1 -0
  142. package/template/components/ui/breadcrumb.tsx +1 -0
  143. package/template/components/ui/button.tsx +1 -0
  144. package/template/components/ui/calendar.tsx +1 -0
  145. package/template/components/ui/card.tsx +1 -0
  146. package/template/components/ui/chart.tsx +1 -0
  147. package/template/components/ui/checkbox.tsx +1 -0
  148. package/template/components/ui/coach-mark.tsx +1 -0
  149. package/template/components/ui/collapsible.tsx +1 -0
  150. package/template/components/ui/command.tsx +1 -0
  151. package/template/components/ui/date-picker-field.tsx +1 -0
  152. package/template/components/ui/dialog.tsx +1 -0
  153. package/template/components/ui/dot-pattern.tsx +159 -0
  154. package/template/components/ui/drag-handle-grip.tsx +1 -0
  155. package/template/components/ui/drawer.tsx +1 -0
  156. package/template/components/ui/dropdown-menu.tsx +1 -0
  157. package/template/components/ui/field.tsx +1 -0
  158. package/template/components/ui/form.tsx +1 -0
  159. package/template/components/ui/input-group.tsx +1 -0
  160. package/template/components/ui/input-mask.tsx +1 -0
  161. package/template/components/ui/input.tsx +1 -0
  162. package/template/components/ui/kbd.tsx +1 -0
  163. package/template/components/ui/label.tsx +1 -0
  164. package/template/components/ui/leo-icon.tsx +726 -0
  165. package/template/components/ui/payment-card-fields.tsx +1 -0
  166. package/template/components/ui/popover.tsx +1 -0
  167. package/template/components/ui/radio-group.tsx +1 -0
  168. package/template/components/ui/select.tsx +1 -0
  169. package/template/components/ui/selection-tile-grid.tsx +1 -0
  170. package/template/components/ui/separator.tsx +1 -0
  171. package/template/components/ui/sheet.tsx +1 -0
  172. package/template/components/ui/sidebar.tsx +1 -0
  173. package/template/components/ui/skeleton.tsx +1 -0
  174. package/template/components/ui/sonner.tsx +1 -0
  175. package/template/components/ui/status-badge.tsx +1 -0
  176. package/template/components/ui/table.tsx +1 -0
  177. package/template/components/ui/tabs.tsx +1 -0
  178. package/template/components/ui/textarea.tsx +1 -0
  179. package/template/components/ui/tip.tsx +1 -0
  180. package/template/components/ui/toggle-group.tsx +1 -0
  181. package/template/components/ui/toggle-switch.tsx +1 -0
  182. package/template/components/ui/toggle.tsx +1 -0
  183. package/template/components/ui/tooltip.tsx +1 -0
  184. package/template/components/ui/view-segmented-control.tsx +1 -0
  185. package/template/components.json +27 -0
  186. package/template/contexts/chart-variant-context.tsx +35 -0
  187. package/template/contexts/command-menu-context.tsx +28 -0
  188. package/template/contexts/dashboard-view-context.tsx +35 -0
  189. package/template/contexts/product-context.tsx +38 -0
  190. package/template/contexts/system-banner-context.tsx +127 -0
  191. package/template/docs/command-menu-pattern.md +45 -0
  192. package/template/docs/data-views-pattern.md +160 -0
  193. package/template/ecosystem.config.cjs +20 -0
  194. package/template/eslint.config.mjs +18 -0
  195. package/template/fontawesome-subset.manifest.json +190 -0
  196. package/template/hooks/.gitkeep +0 -0
  197. package/template/hooks/use-app-theme.ts +1 -0
  198. package/template/hooks/use-coach-mark.ts +1 -0
  199. package/template/hooks/use-mobile.ts +1 -0
  200. package/template/hooks/use-mod-key-label.ts +1 -0
  201. package/template/lib/.gitkeep +0 -0
  202. package/template/lib/ask-leo-route-context.ts +133 -0
  203. package/template/lib/chart-keyboard-selection.test.ts +20 -0
  204. package/template/lib/chart-keyboard-selection.ts +17 -0
  205. package/template/lib/chart-line-dash.ts +16 -0
  206. package/template/lib/coach-mark-registry.ts +68 -0
  207. package/template/lib/command-menu-config.ts +127 -0
  208. package/template/lib/command-menu-search-data.ts +44 -0
  209. package/template/lib/conditional-rule-match.ts +32 -0
  210. package/template/lib/dashboard-customize-coach-mark.ts +18 -0
  211. package/template/lib/dashboard-layout-merge.ts +63 -0
  212. package/template/lib/data-list-display-options.ts +35 -0
  213. package/template/lib/data-list-persistence.ts +280 -0
  214. package/template/lib/data-list-view-surface.ts +58 -0
  215. package/template/lib/data-list-view.ts +29 -0
  216. package/template/lib/data-view-dashboard-storage.ts +101 -0
  217. package/template/lib/date-filter.ts +8 -0
  218. package/template/lib/dev-log.test.ts +28 -0
  219. package/template/lib/dev-log.ts +8 -0
  220. package/template/lib/editable-target.ts +10 -0
  221. package/template/lib/floating-sheet-panel.ts +72 -0
  222. package/template/lib/initials-from-name.ts +7 -0
  223. package/template/lib/list-page-table-properties.ts +52 -0
  224. package/template/lib/list-status-badges.ts +168 -0
  225. package/template/lib/logo-dev.ts +12 -0
  226. package/template/lib/mock/compliance-kpi.ts +61 -0
  227. package/template/lib/mock/compliance.ts +146 -0
  228. package/template/lib/mock/dashboard.ts +105 -0
  229. package/template/lib/mock/navigation.tsx +231 -0
  230. package/template/lib/mock/placements-kpi.ts +134 -0
  231. package/template/lib/mock/placements.ts +183 -0
  232. package/template/lib/mock/question-bank-kpi.ts +61 -0
  233. package/template/lib/mock/question-bank.ts +142 -0
  234. package/template/lib/mock/sites-directory.ts +16 -0
  235. package/template/lib/mock/sites-kpi.ts +25 -0
  236. package/template/lib/mock/team-kpi.ts +60 -0
  237. package/template/lib/mock/team.ts +118 -0
  238. package/template/lib/motion-ui.ts +17 -0
  239. package/template/lib/placement-board-card-layout.ts +79 -0
  240. package/template/lib/placement-lifecycle.ts +5 -0
  241. package/template/lib/row-height.ts +10 -0
  242. package/template/lib/stock-portrait.ts +11 -0
  243. package/template/lib/utils.test.ts +13 -0
  244. package/template/lib/utils.ts +1 -0
  245. package/template/next.config.mjs +15 -0
  246. package/template/package.json +83 -0
  247. package/template/postcss.config.mjs +8 -0
  248. package/template/public/.gitkeep +0 -0
  249. package/template/public/Illustration/Rotation.svg +74 -0
  250. package/template/public/avatars/user.svg +11 -0
  251. package/template/public/favicon/favicon.ico +0 -0
  252. package/template/public/favicon.ico +0 -0
  253. package/template/public/logos/exxat-one.svg +36 -0
  254. package/template/public/logos/exxat-prism.svg +39 -0
  255. package/template/public/mock-schools/emory.svg +4 -0
  256. package/template/public/mock-schools/rush.svg +4 -0
  257. package/template/scripts/fontawesome-subset-audit.mjs +190 -0
  258. package/template/scripts/pm2-startup-macos.sh +13 -0
  259. package/template/skills-lock.json +10 -0
  260. package/template/stores/app-store.ts +33 -0
  261. package/template/tests/setup.ts +1 -0
  262. package/template/tsconfig.json +35 -0
  263. package/template/types/react-payment-inputs.d.ts +19 -0
  264. package/template/vitest.config.ts +18 -0
@@ -0,0 +1,251 @@
1
+ "use client"
2
+ import * as React from "react"
3
+ import { cn } from "@/lib/utils"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Input } from "@/components/ui/input"
6
+ import { Tip } from "@/components/ui/tip"
7
+ import { FilterDateCalendar } from "@/components/data-table/filter-date-calendar"
8
+ import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
9
+ import {
10
+ type ActiveFilter,
11
+ type ConditionalRule,
12
+ type FilterFieldDef,
13
+ type FilterOperator,
14
+ OPERATOR_LABELS,
15
+ RULE_COLORS,
16
+ } from "./types"
17
+
18
+ type DrawerFilterCardBaseProps = {
19
+ fieldDef: FilterFieldDef
20
+ expanded: boolean
21
+ onToggleExpand: () => void
22
+ onRemove: (id: string) => void
23
+ renderOptionLabel?: (value: string) => React.ReactNode
24
+ }
25
+
26
+ export type DrawerFilterCardProps =
27
+ | (DrawerFilterCardBaseProps & {
28
+ variant?: "filter"
29
+ filter: ActiveFilter
30
+ onUpdate: (id: string, patch: Partial<ActiveFilter>) => void
31
+ })
32
+ | (DrawerFilterCardBaseProps & {
33
+ variant: "conditional"
34
+ filter: ConditionalRule
35
+ onUpdate: (id: string, patch: Partial<ConditionalRule>) => void
36
+ })
37
+
38
+ /** Inline filter card used inside the Table Properties drawer (filter or conditional rule). */
39
+ export function DrawerFilterCard(props: DrawerFilterCardProps) {
40
+ const {
41
+ fieldDef,
42
+ expanded,
43
+ onToggleExpand,
44
+ onRemove,
45
+ renderOptionLabel,
46
+ } = props
47
+
48
+ const isCond = props.variant === "conditional"
49
+ const filter = props.filter
50
+ const filterId = filter.id
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const onUpdate = props.onUpdate as (id: string, patch: any) => void
53
+
54
+ const [optSearch, setOptSearch] = React.useState("")
55
+ const options = fieldDef.options ?? []
56
+ const showSearch = options.length > 8
57
+ const filteredOpts = optSearch
58
+ ? options.filter(o => o.label.toLowerCase().includes(optSearch.toLowerCase()))
59
+ : options
60
+
61
+ const values = filter.values
62
+
63
+ React.useEffect(() => {
64
+ if (fieldDef.type !== "select" && fieldDef.type !== "date") return
65
+ if (filter.operator !== "is" && filter.operator !== "is_not") {
66
+ onUpdate(filterId, { operator: "is" })
67
+ }
68
+ }, [filter.operator, filter.id, fieldDef.type, filterId, onUpdate])
69
+
70
+ function toggleValue(val: string) {
71
+ const next = values.includes(val) ? values.filter(v => v !== val) : [...values, val]
72
+ onUpdate(filterId, { values: next })
73
+ }
74
+
75
+ function cycleOperator() {
76
+ const ops = fieldDef.operators
77
+ const idx = ops.indexOf(filter.operator as FilterOperator)
78
+ const i = idx === -1 ? 0 : idx
79
+ onUpdate(filterId, { operator: ops[(i + 1) % ops.length] })
80
+ }
81
+
82
+ const removeLabel = isCond ? "rule" : "filter"
83
+ const rule = isCond ? (props.filter as ConditionalRule) : null
84
+
85
+ return (
86
+ <div className="rounded-lg border border-border overflow-hidden">
87
+ <div>
88
+ {/* Card header */}
89
+ <div
90
+ className="flex items-start justify-between px-3 pt-2.5 pb-2 gap-2 cursor-pointer"
91
+ role="button"
92
+ tabIndex={0}
93
+ aria-label={expanded ? `Collapse ${fieldDef.label}` : `Expand ${fieldDef.label}`}
94
+ onClick={onToggleExpand}
95
+ onKeyDown={e => {
96
+ if (e.key === "Enter" || e.key === " ") {
97
+ e.preventDefault()
98
+ onToggleExpand()
99
+ }
100
+ }}
101
+ >
102
+ <div className="flex-1 min-w-0">
103
+ <p className="text-sm font-semibold text-foreground">{fieldDef.label}</p>
104
+ <Button
105
+ type="button"
106
+ variant="ghost"
107
+ size="xs"
108
+ aria-label={`Operator: ${OPERATOR_LABELS[filter.operator as FilterOperator]} — click to cycle`}
109
+ onClick={e => {
110
+ e.stopPropagation()
111
+ cycleOperator()
112
+ }}
113
+ className="h-auto py-0 px-1 -ms-1 text-xs text-muted-foreground font-normal"
114
+ >
115
+ {OPERATOR_LABELS[filter.operator as FilterOperator]}
116
+ <i className="fa-light fa-chevron-down text-xs" aria-hidden="true" />
117
+ </Button>
118
+ </div>
119
+ <div className="flex items-center gap-0.5 shrink-0 self-start">
120
+ <Tip label={`Remove ${fieldDef.label} ${removeLabel}`} side="top">
121
+ <Button
122
+ type="button"
123
+ variant="ghost"
124
+ size="icon-sm"
125
+ aria-label={`Remove ${fieldDef.label} ${removeLabel}`}
126
+ className="text-muted-foreground hover:text-destructive"
127
+ onClick={e => {
128
+ e.stopPropagation()
129
+ onRemove(filterId)
130
+ }}
131
+ >
132
+ <i className="fa-light fa-trash text-xs" aria-hidden="true" />
133
+ </Button>
134
+ </Tip>
135
+ <i
136
+ className={`fa-light ${expanded ? "fa-chevron-up" : "fa-chevron-down"} text-xs text-muted-foreground mt-2`}
137
+ aria-hidden="true"
138
+ />
139
+ </div>
140
+ </div>
141
+
142
+ {/* Expanded body */}
143
+ {expanded && (
144
+ <div className="border-t border-border">
145
+ {fieldDef.type === "select" ? (
146
+ <>
147
+ {showSearch && (
148
+ <div className="px-3 pt-2 pb-1">
149
+ <Input placeholder="Search…" value={optSearch} onChange={e => setOptSearch(e.target.value)} className="h-7 text-xs" />
150
+ </div>
151
+ )}
152
+ <div role="listbox" aria-multiselectable="true" aria-label={`${fieldDef.label} options`} className="py-1 max-h-52 overflow-y-auto">
153
+ {filteredOpts.map(opt => {
154
+ const checked = values.includes(opt.value)
155
+ return (
156
+ <div
157
+ key={opt.value}
158
+ role="option"
159
+ aria-selected={checked}
160
+ tabIndex={0}
161
+ onClick={() => toggleValue(opt.value)}
162
+ onKeyDown={e => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleValue(opt.value) } }}
163
+ className="flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-interactive-hover cursor-pointer select-none focus-visible:outline-none focus-visible:bg-interactive-hover"
164
+ >
165
+ <span aria-hidden="true" data-slot="checkbox" data-state={checked ? "checked" : "unchecked"} className={cn(
166
+ "inline-flex items-center justify-center size-3.5 shrink-0 rounded-[3px] border transition-colors",
167
+ checked ? "bg-primary border-primary text-primary-foreground" : "border-input bg-background"
168
+ )}>
169
+ {checked && <i className="fa-solid fa-check text-current" style={{ fontSize: "7px" }} />}
170
+ </span>
171
+ {renderOptionLabel
172
+ ? renderOptionLabel(opt.value)
173
+ : <span className="text-foreground">{opt.label}</span>
174
+ }
175
+ </div>
176
+ )
177
+ })}
178
+ {filteredOpts.length === 0 && (
179
+ <p className="px-3 py-2 text-xs text-muted-foreground">No options found</p>
180
+ )}
181
+ </div>
182
+ </>
183
+ ) : fieldDef.type === "date" ? (
184
+ <div className="p-2">
185
+ <FilterDateCalendar
186
+ label={`${fieldDef.label} — choose date`}
187
+ valueYmd={filter.values[0]}
188
+ onChangeYmd={(ymd) =>
189
+ onUpdate(filterId, { values: ymd ? [ymd] : [] })
190
+ }
191
+ />
192
+ </div>
193
+ ) : fieldDef.type === "text" ? (
194
+ <div className="p-3">
195
+ <FilterTextValueInput
196
+ mask={fieldDef.textMask}
197
+ aria-label={`${fieldDef.label} value`}
198
+ placeholder={`Enter ${fieldDef.label.toLowerCase()}…`}
199
+ value={values[0] ?? ""}
200
+ onValueChange={next => onUpdate(filterId, { values: [next] })}
201
+ className="text-sm"
202
+ autoFocus
203
+ />
204
+ </div>
205
+ ) : null}
206
+ {values.length > 0 ? (
207
+ <div className="sticky bottom-0 border-t border-border bg-card p-2">
208
+ <Button
209
+ type="button"
210
+ variant="outline"
211
+ size="sm"
212
+ onClick={() => onUpdate(filterId, { values: [] })}
213
+ className="w-full justify-center gap-1.5 text-xs text-muted-foreground"
214
+ >
215
+ <i className="fa-light fa-xmark text-xs" aria-hidden="true" />
216
+ Clear selection
217
+ </Button>
218
+ </div>
219
+ ) : null}
220
+ </div>
221
+ )}
222
+
223
+ {/* Highlight color — conditional rules only */}
224
+ {isCond && rule && (
225
+ <div className="border-t border-border px-3 py-2.5">
226
+ <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2">
227
+ Highlight color
228
+ </p>
229
+ <div className="flex flex-wrap gap-1.5">
230
+ {RULE_COLORS.map(c => (
231
+ <Button
232
+ key={c.name}
233
+ type="button"
234
+ size="icon-xs"
235
+ variant="outline"
236
+ aria-label={c.name}
237
+ className={cn(
238
+ "rounded-md border-2 p-0 transition-all",
239
+ rule.bgColor === c.bg ? "border-foreground scale-110" : "border-transparent hover:scale-105",
240
+ )}
241
+ style={{ background: c.bg }}
242
+ onClick={() => onUpdate(filterId, { bgColor: c.bg })}
243
+ />
244
+ ))}
245
+ </div>
246
+ </div>
247
+ )}
248
+ </div>
249
+ </div>
250
+ )
251
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Table / data properties drawer — configure filters, sort, columns, display, and view type.
3
+ * Pass column metadata and filter field definitions for any row shape; placement defaults live in types.ts.
4
+ *
5
+ * List page ↔ Properties: `createListPageEditViewHandler` + `OpenTablePropertiesHandle`, or pass
6
+ * `tablePropertiesRef` on `ListPageTemplate` (see `lib/list-page-table-properties.ts`).
7
+ */
8
+
9
+ export { TablePropertiesDrawer } from "./drawer"
10
+ export type { TablePropertiesDrawerProps } from "./drawer"
11
+ export { TablePropertiesDrawerButton } from "./drawer-button"
12
+ export type {
13
+ TablePropertiesDrawerButtonProps,
14
+ TablePropertiesDrawerButtonState,
15
+ } from "./drawer-button"
16
+ export * from "./types"
17
+
18
+ export {
19
+ createListPageEditViewHandler,
20
+ isDataListSurfaceViewType,
21
+ type OpenTablePropertiesHandle,
22
+ } from "@/lib/list-page-table-properties"
@@ -0,0 +1,59 @@
1
+ "use client"
2
+ import * as React from "react"
3
+ import { Tip } from "@/components/ui/tip"
4
+ import { DragHandleGripIcon } from "@/components/ui/drag-handle-grip"
5
+ import { type SortRule, COLUMNS } from "./types"
6
+
7
+ /** Sort rule card inside the Sort drawer panel */
8
+ export type DrawerSortCardProps = {
9
+ rule: SortRule
10
+ /** When the active table uses dynamic columns (e.g. placements), pass the resolved label. */
11
+ fieldLabel?: string
12
+ isPrimary: boolean
13
+ onRemove: () => void
14
+ onToggleDir: () => void
15
+ }
16
+
17
+ export function DrawerSortCard(props: DrawerSortCardProps) {
18
+ const { rule, fieldLabel, isPrimary, onRemove, onToggleDir } = props
19
+ const col = COLUMNS.find(c => c.key === rule.fieldKey)
20
+ const label = fieldLabel ?? col?.label ?? rule.fieldKey
21
+ if (!label) return null
22
+ return (
23
+ <div className="rounded-lg border border-border bg-background overflow-hidden">
24
+ <div className="flex items-center gap-2 px-3 py-2.5">
25
+ <DragHandleGripIcon className="text-[13px] text-muted-foreground/40" />
26
+ <div className="flex-1 min-w-0">
27
+ <div className="flex items-center gap-1.5">
28
+ {isPrimary && (
29
+ <span className="text-xs font-bold text-accent-foreground bg-accent rounded px-1 py-0.5 leading-none uppercase tracking-wide shrink-0">
30
+ Primary
31
+ </span>
32
+ )}
33
+ <p className="text-sm font-medium text-foreground truncate">{label}</p>
34
+ </div>
35
+ <button
36
+ type="button"
37
+ aria-label={`Direction: ${rule.direction === "asc" ? "Ascending" : "Descending"} — click to toggle`}
38
+ onClick={onToggleDir}
39
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-interactive-hover-foreground transition-colors mt-0.5"
40
+ >
41
+ <i className={`fa-light ${rule.direction === "asc" ? "fa-arrow-up-az" : "fa-arrow-down-az"} text-xs`} aria-hidden="true" />
42
+ {rule.direction === "asc" ? "Ascending" : "Descending"}
43
+ <i className="fa-light fa-chevron-down text-xs" aria-hidden="true" />
44
+ </button>
45
+ </div>
46
+ <Tip label={`Remove ${label} sort`} side="top">
47
+ <button
48
+ type="button"
49
+ aria-label={`Remove ${label} sort`}
50
+ onClick={onRemove}
51
+ className="inline-flex items-center justify-center size-7 rounded text-muted-foreground hover:text-destructive hover:bg-interactive-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0"
52
+ >
53
+ <i className="fa-light fa-trash text-xs" aria-hidden="true" />
54
+ </button>
55
+ </Tip>
56
+ </div>
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,124 @@
1
+ "use client"
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // Shared types for table-properties components
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ export type FilterOperator = "is" | "is_not" | "contains" | "not_contains"
8
+
9
+ /** Input mask for `type: "text"` filters — [Shadcn Studio input-mask](https://shadcnstudio.com/docs/components/input-mask). */
10
+ export type FilterTextMask = "phone" | "zip" | "dateMDY"
11
+
12
+ export interface FilterFieldDef {
13
+ key: string
14
+ label: string
15
+ icon: string
16
+ type: "select" | "text" | "date"
17
+ operators: FilterOperator[]
18
+ /** Select options, or for `date` fields used by conditional rules (exact row strings). */
19
+ options?: { value: string; label: string }[]
20
+ /** When `type` is `text`, optional `use-mask-input` pattern for the value field. */
21
+ textMask?: FilterTextMask
22
+ }
23
+
24
+ export interface ActiveFilter {
25
+ id: string
26
+ fieldKey: string
27
+ operator: FilterOperator
28
+ values: string[]
29
+ }
30
+
31
+ export interface SortRule {
32
+ id: string
33
+ fieldKey: string
34
+ direction: "asc" | "desc"
35
+ }
36
+
37
+ export const OPERATOR_LABELS: Record<FilterOperator, string> = {
38
+ is: "is", is_not: "is not", contains: "contains", not_contains: "does not contain",
39
+ }
40
+
41
+ /** Default filter field list (placement table uses column-derived defs via `filterFields` prop). */
42
+ export const FILTER_FIELDS: FilterFieldDef[] = [
43
+ { key: "student", label: "Student", icon: "fa-user", type: "text", operators: ["contains", "not_contains"] },
44
+ {
45
+ key: "specialization", label: "Specialization", icon: "fa-stethoscope", type: "select",
46
+ operators: ["is", "is_not"],
47
+ options: [
48
+ { value: "Adult Health", label: "Adult Health" },
49
+ { value: "Orthopedics", label: "Orthopedics" },
50
+ { value: "Hand Therapy", label: "Hand Therapy" },
51
+ { value: "Critical Care", label: "Critical Care" },
52
+ { value: "Behavioral Health", label: "Behavioral Health" },
53
+ { value: "Sports Rehab", label: "Sports Rehab" },
54
+ { value: "Pediatrics", label: "Pediatrics" },
55
+ { value: "Neuro", label: "Neuro" },
56
+ { value: "Family Practice", label: "Family Practice" },
57
+ { value: "Neuro Rehab", label: "Neuro Rehab" },
58
+ { value: "Youth Services", label: "Youth Services" },
59
+ { value: "Emergency", label: "Emergency" },
60
+ { value: "Acute Care", label: "Acute Care" },
61
+ { value: "Women's Health", label: "Women's Health" },
62
+ ],
63
+ },
64
+ { key: "site", label: "Site", icon: "fa-hospital", type: "text", operators: ["contains", "not_contains"] },
65
+ {
66
+ key: "status", label: "Status", icon: "fa-circle-dot", type: "select",
67
+ operators: ["is", "is_not"],
68
+ options: [
69
+ { value: "confirmed", label: "Confirmed" },
70
+ { value: "pending", label: "Pending" },
71
+ { value: "under-review", label: "Under Review" },
72
+ { value: "rejected", label: "Rejected" },
73
+ { value: "completed", label: "Completed" },
74
+ ],
75
+ },
76
+ { key: "start", label: "Start Date", icon: "fa-calendar", type: "date", operators: ["is", "is_not"] },
77
+ { key: "supervisor", label: "Supervisor", icon: "fa-user-tie", type: "text", operators: ["contains", "not_contains"] },
78
+ ]
79
+
80
+ // Column definitions — shared with drawer
81
+ export interface ColDef {
82
+ key: string
83
+ label: string
84
+ sortable: boolean
85
+ sortKey?: string
86
+ minWidth: number
87
+ }
88
+
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // Conditional formatting rules
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+
93
+ export interface ConditionalRule {
94
+ id: string
95
+ /** Column key to evaluate */
96
+ fieldKey: string
97
+ operator: FilterOperator
98
+ /** Selected option values (select) or text (single entry) when operator needs values */
99
+ values: string[]
100
+ /** Resolved CSS background color string */
101
+ bgColor: string
102
+ }
103
+
104
+ /** Predefined palette for conditional rule backgrounds */
105
+ export const RULE_COLORS: { name: string; bg: string }[] = [
106
+ { name: "Green", bg: "var(--conditional-rule-green)" },
107
+ { name: "Yellow", bg: "var(--conditional-rule-yellow)" },
108
+ { name: "Blue", bg: "var(--conditional-rule-blue)" },
109
+ { name: "Red", bg: "var(--conditional-rule-red)" },
110
+ { name: "Purple", bg: "var(--conditional-rule-purple)" },
111
+ { name: "Orange", bg: "var(--conditional-rule-orange)" },
112
+ ]
113
+
114
+ export const COLUMNS: ColDef[] = [
115
+ { key: "select", label: "", sortable: false, minWidth: 40 },
116
+ { key: "student", label: "Student", sortable: true, minWidth: 180, sortKey: "student" },
117
+ { key: "specialization", label: "Specialization", sortable: true, minWidth: 100, sortKey: "specialization" },
118
+ { key: "site", label: "Site", sortable: true, minWidth: 100, sortKey: "site" },
119
+ { key: "status", label: "Status", sortable: true, minWidth: 110, sortKey: "status" },
120
+ { key: "start", label: "Start Date", sortable: true, minWidth: 110, sortKey: "start" },
121
+ { key: "duration", label: "Duration", sortable: false, minWidth: 80 },
122
+ { key: "supervisor", label: "Supervisor", sortable: false, minWidth: 100 },
123
+ { key: "actions", label: "", sortable: false, minWidth: 88 },
124
+ ]
@@ -0,0 +1,98 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Badge } from "@/components/ui/badge"
5
+ import { Card, CardContent, CardHeader } from "@/components/ui/card"
6
+ import { Checkbox } from "@/components/ui/checkbox"
7
+ import { Label } from "@/components/ui/label"
8
+ import { TaskPriorityBadge } from "@/components/task-priority-badge"
9
+ import { DashboardSectionTitle } from "@/components/dashboard-section-heading"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ export interface TaskListItem {
13
+ id: number
14
+ label: string
15
+ due: string
16
+ priority: "high" | "medium" | "low"
17
+ done: boolean
18
+ }
19
+
20
+ export function TaskListPanel({
21
+ title = "Tasks",
22
+ headingId,
23
+ headingLevel = "h2",
24
+ plain = false,
25
+ defaultTasks,
26
+ }: {
27
+ title?: string
28
+ headingId?: string
29
+ headingLevel?: "h1" | "h2"
30
+ plain?: boolean
31
+ defaultTasks: TaskListItem[]
32
+ }) {
33
+ const [tasks, setTasks] = React.useState<TaskListItem[]>(defaultTasks)
34
+ const pending = tasks.filter((task) => !task.done).length
35
+
36
+ const header = (
37
+ <div className="flex items-center justify-between gap-2">
38
+ <DashboardSectionTitle as={headingLevel} id={headingId}>
39
+ {title}
40
+ </DashboardSectionTitle>
41
+ <Badge variant="outline" className="text-xs tabular-nums">
42
+ {pending} pending
43
+ </Badge>
44
+ </div>
45
+ )
46
+
47
+ const rows = tasks.map((task) => {
48
+ const taskDomId = `task-${task.id}`
49
+ return (
50
+ <div
51
+ key={task.id}
52
+ className={cn(
53
+ "flex items-start gap-2.5 rounded-lg px-2 py-2 transition-colors hover:bg-interactive-hover-medium",
54
+ task.done && "opacity-80",
55
+ )}
56
+ >
57
+ <Checkbox
58
+ id={taskDomId}
59
+ checked={task.done}
60
+ onCheckedChange={(checked) =>
61
+ setTasks((prev) =>
62
+ prev.map((current) =>
63
+ current.id === task.id ? { ...current, done: checked === true } : current,
64
+ ),
65
+ )
66
+ }
67
+ className="mt-0.5"
68
+ aria-label={`${task.done ? "Mark incomplete" : "Mark complete"}: ${task.label}`}
69
+ />
70
+ <Label htmlFor={taskDomId} className="min-w-0 flex-1 cursor-pointer flex-col items-stretch gap-0.5 font-normal">
71
+ <span className={cn("text-xs font-medium leading-snug text-foreground", task.done && "text-muted-foreground line-through")}>
72
+ {task.label}
73
+ </span>
74
+ <span className="text-xs text-muted-foreground">{task.due}</span>
75
+ </Label>
76
+ <div className="shrink-0 pt-0.5">
77
+ <TaskPriorityBadge priority={task.priority} />
78
+ </div>
79
+ </div>
80
+ )
81
+ })
82
+
83
+ if (plain) {
84
+ return (
85
+ <section aria-labelledby={headingId} className="flex flex-col gap-3">
86
+ {header}
87
+ <div className="flex flex-col gap-0.5">{rows}</div>
88
+ </section>
89
+ )
90
+ }
91
+
92
+ return (
93
+ <Card size="sm">
94
+ <CardHeader>{header}</CardHeader>
95
+ <CardContent className="flex flex-col gap-0.5">{rows}</CardContent>
96
+ </Card>
97
+ )
98
+ }
@@ -0,0 +1,28 @@
1
+ "use client"
2
+
3
+ import { Badge } from "@/components/ui/badge"
4
+ import {
5
+ normalizeTaskPriority,
6
+ TASK_PRIORITY_BADGE_CLASS,
7
+ TASK_PRIORITY_LABEL,
8
+ } from "@/lib/list-status-badges"
9
+ import { cn } from "@/lib/utils"
10
+
11
+ export function TaskPriorityBadge({ priority }: { priority: string }) {
12
+ const level = normalizeTaskPriority(priority)
13
+ if (!level) {
14
+ return (
15
+ <Badge variant="outline" className="capitalize text-xs">
16
+ {priority}
17
+ </Badge>
18
+ )
19
+ }
20
+ return (
21
+ <Badge
22
+ variant="outline"
23
+ className={cn("text-xs", TASK_PRIORITY_BADGE_CLASS[level])}
24
+ >
25
+ {TASK_PRIORITY_LABEL[level]}
26
+ </Badge>
27
+ )
28
+ }