@exxatdesignux/ui 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- 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
|
+
}
|