@exxatdesignux/ui 0.1.0 → 0.2.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/cli.mjs +176 -0
- package/bin/init.mjs +15 -1
- package/bin/sync-extras.mjs +65 -0
- package/consumer-extras/README.md +21 -0
- package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +282 -0
- package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +68 -0
- package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +99 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +713 -0
- package/consumer-extras/cursor-skills/exxat-fontawesome-icons/SKILL.md +31 -0
- package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +36 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +27 -0
- package/consumer-extras/patterns/command-menu-pattern.md +45 -0
- package/consumer-extras/patterns/data-views-pattern.md +167 -0
- package/package.json +7 -3
- package/src/components/ui/sidebar.tsx +7 -2
- package/template/.agents/skills/shadcn/SKILL.md +242 -0
- package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
- package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/template/.agents/skills/shadcn/cli.md +257 -0
- package/template/.agents/skills/shadcn/customization.md +202 -0
- package/template/.agents/skills/shadcn/evals/evals.json +47 -0
- package/template/.agents/skills/shadcn/mcp.md +94 -0
- package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/template/.agents/skills/shadcn/rules/composition.md +195 -0
- package/template/.agents/skills/shadcn/rules/forms.md +192 -0
- package/template/.agents/skills/shadcn/rules/icons.md +101 -0
- package/template/.agents/skills/shadcn/rules/styling.md +162 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
- package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
- package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
- package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
- package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
- package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
- package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
- package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
- package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
- package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
- package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
- package/template/AGENTS.md +52 -11
- package/template/app/(app)/dashboard/page.tsx +1 -1
- package/template/app/(app)/data-list/[id]/page.tsx +24 -8
- package/template/app/(app)/data-list/new/page.tsx +7 -4
- package/template/app/(app)/data-list/page.tsx +1 -1
- package/template/app/(app)/examples/page.tsx +41 -0
- package/template/app/(app)/question-bank/page.tsx +3 -3
- package/template/app/globals.css +1 -1
- package/template/components/app-sidebar.tsx +52 -35
- package/template/components/compliance-table.tsx +79 -0
- package/template/components/data-list-client.tsx +36 -25
- package/template/components/data-list-table.tsx +797 -10
- package/template/components/data-views/finder-panel-view.tsx +405 -0
- package/template/components/data-views/folder-grid-view.tsx +86 -0
- package/template/components/data-views/index.ts +59 -0
- package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
- package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
- package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
- package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
- package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
- package/template/components/data-views/list-page-view-frame.tsx +53 -0
- package/template/components/data-views/os-folder-glyph.tsx +121 -0
- package/template/components/folder-details-shell.tsx +230 -0
- package/template/components/hub-tree-panel-view.tsx +672 -0
- package/template/components/list-hub-status-badge.tsx +17 -3
- package/template/components/page-header.tsx +149 -7
- package/template/components/placements-page-header.tsx +14 -8
- package/template/components/placements-table-columns.tsx +8 -8
- package/template/components/question-bank-client.tsx +157 -39
- package/template/components/question-bank-new-folder-sheet.tsx +248 -0
- package/template/components/question-bank-os-folder-view.tsx +648 -0
- package/template/components/question-bank-page-header.tsx +31 -2
- package/template/components/question-bank-panel-activator.tsx +9 -0
- package/template/components/question-bank-secondary-nav.tsx +226 -0
- package/template/components/question-bank-table.tsx +707 -22
- package/template/components/secondary-panel.tsx +41 -107
- package/template/components/sites-table.tsx +66 -0
- package/template/components/team-client.tsx +7 -0
- package/template/components/team-table.tsx +156 -1
- package/template/components/templates/list-page.tsx +2 -2
- package/template/components/ui/avatar.tsx +1 -1
- package/template/components/ui/badge.tsx +1 -1
- package/template/components/ui/banner.tsx +1 -1
- package/template/components/ui/breadcrumb.tsx +1 -1
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/calendar.tsx +1 -1
- package/template/components/ui/card.tsx +1 -1
- package/template/components/ui/chart.tsx +1 -1
- package/template/components/ui/checkbox.tsx +1 -1
- package/template/components/ui/coach-mark.tsx +1 -1
- package/template/components/ui/collapsible.tsx +1 -1
- package/template/components/ui/command.tsx +1 -1
- package/template/components/ui/date-picker-field.tsx +1 -1
- package/template/components/ui/dialog.tsx +1 -1
- package/template/components/ui/drag-handle-grip.tsx +1 -1
- package/template/components/ui/drawer.tsx +1 -1
- package/template/components/ui/dropdown-menu.tsx +1 -1
- package/template/components/ui/field.tsx +1 -1
- package/template/components/ui/form.tsx +1 -1
- package/template/components/ui/input-group.tsx +1 -1
- package/template/components/ui/input-mask.tsx +1 -1
- package/template/components/ui/input.tsx +1 -1
- package/template/components/ui/kbd.tsx +1 -1
- package/template/components/ui/label.tsx +1 -1
- package/template/components/ui/payment-card-fields.tsx +1 -1
- package/template/components/ui/popover.tsx +1 -1
- package/template/components/ui/radio-group.tsx +1 -1
- package/template/components/ui/resizable.tsx +68 -0
- package/template/components/ui/select.tsx +1 -1
- package/template/components/ui/selection-tile-grid.tsx +1 -1
- package/template/components/ui/separator.tsx +1 -1
- package/template/components/ui/sheet.tsx +1 -1
- package/template/components/ui/sidebar.tsx +1 -1
- package/template/components/ui/skeleton.tsx +1 -1
- package/template/components/ui/sonner.tsx +1 -1
- package/template/components/ui/status-badge.tsx +1 -1
- package/template/components/ui/table.tsx +1 -1
- package/template/components/ui/tabs.tsx +1 -1
- package/template/components/ui/textarea.tsx +1 -1
- package/template/components/ui/tip.tsx +1 -1
- package/template/components/ui/toggle-group.tsx +1 -1
- package/template/components/ui/toggle-switch.tsx +1 -1
- package/template/components/ui/toggle.tsx +1 -1
- package/template/components/ui/tooltip.tsx +1 -1
- package/template/components/ui/view-segmented-control.tsx +1 -1
- package/template/docs/data-views-pattern.md +7 -0
- package/template/hooks/use-app-theme.ts +1 -1
- package/template/hooks/use-coach-mark.ts +1 -1
- package/template/hooks/use-location-hash.ts +15 -0
- package/template/hooks/use-mobile.ts +1 -1
- package/template/hooks/use-mod-key-label.ts +1 -1
- package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
- package/template/lib/ask-leo-route-context.ts +25 -57
- package/template/lib/coach-mark-registry.ts +13 -13
- package/template/lib/command-menu-config.ts +28 -23
- package/template/lib/command-menu-search-data.ts +10 -9
- package/template/lib/data-list-view-surface.ts +12 -1
- package/template/lib/data-list-view.ts +6 -3
- package/template/lib/date-filter.ts +1 -1
- package/template/lib/mock/dashboard.ts +11 -11
- package/template/lib/mock/navigation.tsx +22 -63
- package/template/lib/mock/placements-kpi.ts +19 -19
- package/template/lib/mock/question-bank-folders.ts +167 -0
- package/template/lib/mock/question-bank-header-collaborators.ts +14 -0
- package/template/lib/mock/question-bank-inspector.ts +109 -0
- package/template/lib/mock/question-bank-kpi.ts +1 -1
- package/template/lib/mock/question-bank.ts +80 -0
- package/template/lib/question-bank-nav.ts +91 -0
- package/template/lib/utils.ts +1 -1
- package/template/next.config.mjs +8 -0
- package/template/package.json +1 -0
- package/template/public/folders/icons8-folder-windows-11.svg +1 -0
- package/template/app/(app)/compliance/page.tsx +0 -10
- package/template/app/(app)/rotations/page.tsx +0 -15
- package/template/app/(app)/sites/all/page.tsx +0 -13
- package/template/app/(app)/team/page.tsx +0 -10
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import * as React from "react"
|
|
12
|
-
import { cn }
|
|
13
|
-
import { useSidebar }
|
|
14
|
-
import { Tip }
|
|
15
|
-
import { Button }
|
|
12
|
+
import { cn } from "@/lib/utils"
|
|
13
|
+
import { useSidebar } from "@/components/ui/sidebar"
|
|
14
|
+
import { Tip } from "@/components/ui/tip"
|
|
15
|
+
import { Button } from "@/components/ui/button"
|
|
16
16
|
import {
|
|
17
17
|
InputGroup,
|
|
18
18
|
InputGroupAddon,
|
|
19
19
|
InputGroupInput,
|
|
20
20
|
} from "@/components/ui/input-group"
|
|
21
|
+
import { QuestionBankSecondaryNav } from "@/components/question-bank-secondary-nav"
|
|
21
22
|
|
|
22
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
24
|
// Context
|
|
@@ -43,7 +44,7 @@ export function useSecondaryPanel() {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function SecondaryPanelProvider({ children }: { children: React.ReactNode }) {
|
|
46
|
-
const [activePanel, setActivePanel]
|
|
47
|
+
const [activePanel, setActivePanel] = React.useState<string | null>(null)
|
|
47
48
|
const { setOpen } = useSidebar()
|
|
48
49
|
|
|
49
50
|
const openPanel = React.useCallback((id: string) => {
|
|
@@ -56,9 +57,14 @@ export function SecondaryPanelProvider({ children }: { children: React.ReactNode
|
|
|
56
57
|
setOpen(true) // expand main sidebar back
|
|
57
58
|
}, [setOpen])
|
|
58
59
|
|
|
59
|
-
const value = React.useMemo(
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const value = React.useMemo(
|
|
61
|
+
() => ({
|
|
62
|
+
activePanel,
|
|
63
|
+
openPanel,
|
|
64
|
+
closePanel,
|
|
65
|
+
}),
|
|
66
|
+
[activePanel, openPanel, closePanel],
|
|
67
|
+
)
|
|
62
68
|
|
|
63
69
|
return (
|
|
64
70
|
<SecondaryPanelContext.Provider value={value}>
|
|
@@ -68,26 +74,13 @@ export function SecondaryPanelProvider({ children }: { children: React.ReactNode
|
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
-
//
|
|
77
|
+
// SecondaryPanel — the actual rendered panel
|
|
72
78
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
{ id: "all", label: "All Rotations", icon: "fa-folder", iconActive: "fa-folder", meta: "12 active" },
|
|
76
|
-
{ id: "rotation-1", label: "Clinical Nursing — Fall 2026", icon: "fa-folder", iconActive: "fa-folder", meta: "8 students" },
|
|
77
|
-
{ id: "rotation-2", label: "PT Fieldwork — Spring 2026", icon: "fa-folder", iconActive: "fa-folder", meta: "6 students" },
|
|
78
|
-
{ id: "rotation-3", label: "OT Level II — Summer 2026", icon: "fa-folder", iconActive: "fa-folder", meta: "4 students" },
|
|
79
|
-
]
|
|
80
|
-
|
|
81
|
-
function RotationsPanel() {
|
|
80
|
+
function QuestionBankPanel() {
|
|
82
81
|
const { closePanel } = useSecondaryPanel()
|
|
83
|
-
const [activeRotation, setActiveRotation] = React.useState("all")
|
|
84
82
|
const [query, setQuery] = React.useState("")
|
|
85
83
|
|
|
86
|
-
const q = query.trim().toLowerCase()
|
|
87
|
-
const filtered = q
|
|
88
|
-
? ROTATION_ITEMS.filter(i => i.label.toLowerCase().includes(q))
|
|
89
|
-
: ROTATION_ITEMS
|
|
90
|
-
|
|
91
84
|
return (
|
|
92
85
|
<>
|
|
93
86
|
<div className="flex items-center justify-between gap-2 px-4 pt-4 pb-2">
|
|
@@ -95,32 +88,19 @@ function RotationsPanel() {
|
|
|
95
88
|
className="text-xl font-semibold leading-tight text-sidebar-foreground"
|
|
96
89
|
style={{ fontFamily: "var(--font-heading)" }}
|
|
97
90
|
>
|
|
98
|
-
|
|
91
|
+
Question bank
|
|
99
92
|
</h2>
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</Tip>
|
|
112
|
-
<Tip label="Close panel" side="bottom">
|
|
113
|
-
<Button
|
|
114
|
-
type="button"
|
|
115
|
-
size="icon"
|
|
116
|
-
variant="ghost"
|
|
117
|
-
onClick={closePanel}
|
|
118
|
-
aria-label="Close panel"
|
|
119
|
-
>
|
|
120
|
-
<i className="fa-light fa-xmark" aria-hidden="true" />
|
|
121
|
-
</Button>
|
|
122
|
-
</Tip>
|
|
123
|
-
</div>
|
|
93
|
+
<Tip label="Close panel" side="bottom">
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
size="icon"
|
|
97
|
+
variant="ghost"
|
|
98
|
+
onClick={closePanel}
|
|
99
|
+
aria-label="Close panel"
|
|
100
|
+
>
|
|
101
|
+
<i className="fa-light fa-xmark" aria-hidden="true" />
|
|
102
|
+
</Button>
|
|
103
|
+
</Tip>
|
|
124
104
|
</div>
|
|
125
105
|
<div className="px-4 pb-2">
|
|
126
106
|
<InputGroup className="h-8 min-h-8">
|
|
@@ -131,68 +111,20 @@ function RotationsPanel() {
|
|
|
131
111
|
type="search"
|
|
132
112
|
value={query}
|
|
133
113
|
onChange={e => setQuery(e.target.value)}
|
|
134
|
-
placeholder="Search
|
|
135
|
-
aria-label="Search
|
|
114
|
+
placeholder="Search"
|
|
115
|
+
aria-label="Search"
|
|
136
116
|
className="text-sm pe-3"
|
|
137
117
|
/>
|
|
138
118
|
</InputGroup>
|
|
139
119
|
</div>
|
|
140
|
-
<
|
|
141
|
-
{filtered.length === 0 && (
|
|
142
|
-
<li className="py-2 text-xs text-muted-foreground">No results</li>
|
|
143
|
-
)}
|
|
144
|
-
{filtered.map(item => {
|
|
145
|
-
const isActive = item.id === activeRotation
|
|
146
|
-
return (
|
|
147
|
-
<li key={item.id}>
|
|
148
|
-
<Tip label={item.label} side="right">
|
|
149
|
-
<button
|
|
150
|
-
type="button"
|
|
151
|
-
role="option"
|
|
152
|
-
aria-selected={isActive}
|
|
153
|
-
onClick={() => setActiveRotation(item.id)}
|
|
154
|
-
className={cn(
|
|
155
|
-
// Match primary `SidebarMenuButton`: text-sm, compact padding, sidebar tokens
|
|
156
|
-
"flex w-full items-start gap-2 rounded-md p-2 text-left text-sm transition-colors",
|
|
157
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
158
|
-
isActive
|
|
159
|
-
? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
|
|
160
|
-
: "text-sidebar-foreground hover:bg-sidebar-accent/50",
|
|
161
|
-
)}
|
|
162
|
-
>
|
|
163
|
-
<i
|
|
164
|
-
className={cn(
|
|
165
|
-
"mt-0.5 size-4 shrink-0 text-center text-[13px] leading-none",
|
|
166
|
-
isActive ? `fa-solid ${item.iconActive}` : `fa-light ${item.icon}`,
|
|
167
|
-
)}
|
|
168
|
-
aria-hidden="true"
|
|
169
|
-
/>
|
|
170
|
-
<div className="min-w-0 flex-1">
|
|
171
|
-
<span className="block whitespace-normal break-words leading-snug">
|
|
172
|
-
{item.label}
|
|
173
|
-
</span>
|
|
174
|
-
{item.meta && (
|
|
175
|
-
<span className="mt-0.5 block whitespace-normal break-words text-xs leading-snug text-muted-foreground">
|
|
176
|
-
{item.meta}
|
|
177
|
-
</span>
|
|
178
|
-
)}
|
|
179
|
-
</div>
|
|
180
|
-
</button>
|
|
181
|
-
</Tip>
|
|
182
|
-
</li>
|
|
183
|
-
)
|
|
184
|
-
})}
|
|
185
|
-
</ul>
|
|
120
|
+
<QuestionBankSecondaryNav query={query} />
|
|
186
121
|
</>
|
|
187
122
|
)
|
|
188
123
|
}
|
|
189
124
|
|
|
190
|
-
|
|
191
|
-
// SecondaryPanel — the actual rendered panel
|
|
192
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
-
|
|
125
|
+
/** Register panel components by id when a route opts into `secondaryPanel` in nav. */
|
|
194
126
|
const PANELS: Record<string, React.FC> = {
|
|
195
|
-
|
|
127
|
+
"question-bank": QuestionBankPanel,
|
|
196
128
|
}
|
|
197
129
|
|
|
198
130
|
export function SecondaryPanel() {
|
|
@@ -207,18 +139,18 @@ export function SecondaryPanel() {
|
|
|
207
139
|
"flex flex-col overflow-hidden",
|
|
208
140
|
"transition-[width,margin,opacity] duration-200 ease-linear",
|
|
209
141
|
activePanel
|
|
210
|
-
? "w-
|
|
211
|
-
: "h-0 min-h-0 shrink overflow-hidden border-0 p-0 m-0 min-w-0 w-0 max-w-0 opacity-0 pointer-events-none"
|
|
142
|
+
? "w-64 shrink-0 m-2 mx-2 rounded-xl ring-1 ring-sidebar-border shadow-sm relative h-[min(calc(100svh-2rem),800px)] md:sticky md:top-2 md:h-[min(calc(100svh-1rem),800px)]"
|
|
143
|
+
: "h-0 min-h-0 shrink overflow-hidden border-0 p-0 m-0 min-w-0 w-0 max-w-0 opacity-0 pointer-events-none",
|
|
212
144
|
)}
|
|
213
145
|
style={activePanel ? { backgroundColor: "var(--secondary-panel-bg, #FAFBFF)" } : undefined}
|
|
214
146
|
>
|
|
215
147
|
<div
|
|
216
148
|
className={cn(
|
|
217
149
|
"flex flex-1 flex-col overflow-y-auto",
|
|
218
|
-
activePanel ? "min-w-0" : "hidden min-w-0 w-0 p-0"
|
|
150
|
+
activePanel ? "min-w-0" : "hidden min-w-0 w-0 p-0",
|
|
219
151
|
)}
|
|
220
152
|
>
|
|
221
|
-
{PanelContent
|
|
153
|
+
{PanelContent ? <PanelContent /> : null}
|
|
222
154
|
</div>
|
|
223
155
|
</nav>
|
|
224
156
|
)
|
|
@@ -233,7 +165,9 @@ export function useAutoPanel(panelId: string) {
|
|
|
233
165
|
|
|
234
166
|
React.useEffect(() => {
|
|
235
167
|
openPanel(panelId)
|
|
236
|
-
return () => {
|
|
237
|
-
|
|
168
|
+
return () => {
|
|
169
|
+
closePanel()
|
|
170
|
+
}
|
|
171
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
238
172
|
}, [panelId])
|
|
239
173
|
}
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
} from "@/components/ui/dropdown-menu"
|
|
25
25
|
import { Tip } from "@/components/ui/tip"
|
|
26
26
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
|
27
|
+
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
28
|
+
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
27
29
|
import { SitesCardGrid } from "@/components/sites-board-view"
|
|
28
30
|
import { SitesListView } from "@/components/sites-list-view"
|
|
29
31
|
import { KeyMetrics } from "@/components/key-metrics"
|
|
@@ -206,6 +208,48 @@ export const SitesTable = React.forwardRef<
|
|
|
206
208
|
[tableState.rows.length],
|
|
207
209
|
)
|
|
208
210
|
|
|
211
|
+
// Generic panel view rendering for sites
|
|
212
|
+
const panelGroupsBuilder = (rows: SiteDirectoryRow[]): FinderGroup[] => [
|
|
213
|
+
{
|
|
214
|
+
id: "all",
|
|
215
|
+
label: `All sites (${rows.length})`,
|
|
216
|
+
count: rows.length,
|
|
217
|
+
},
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
const panelRenderListRow = (row: SiteDirectoryRow, _isSelected: boolean) => (
|
|
221
|
+
<div className="flex-1 min-w-0 flex items-center gap-2">
|
|
222
|
+
<Avatar size="sm" className="size-6 shrink-0">
|
|
223
|
+
<AvatarFallback className="bg-brand/10 p-0 text-brand text-xs">
|
|
224
|
+
<i className="fa-light fa-hospital text-xs" aria-hidden="true" />
|
|
225
|
+
</AvatarFallback>
|
|
226
|
+
</Avatar>
|
|
227
|
+
<div className="flex-1 min-w-0">
|
|
228
|
+
<p className="text-sm font-medium text-foreground truncate">{row.name}</p>
|
|
229
|
+
<p className="text-xs text-muted-foreground truncate">{row.url}</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
const panelRenderDetail = (row: SiteDirectoryRow) => (
|
|
235
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
|
|
236
|
+
<div>
|
|
237
|
+
<h3 className="text-sm font-semibold text-foreground mb-2">Site</h3>
|
|
238
|
+
<p className="text-sm text-foreground">{row.name}</p>
|
|
239
|
+
</div>
|
|
240
|
+
<div className="flex flex-col gap-2">
|
|
241
|
+
<div>
|
|
242
|
+
<span className="text-xs font-medium text-muted-foreground">Key</span>
|
|
243
|
+
<p className="text-sm text-foreground font-mono">{row.id}</p>
|
|
244
|
+
</div>
|
|
245
|
+
<div>
|
|
246
|
+
<span className="text-xs font-medium text-muted-foreground">Path</span>
|
|
247
|
+
<p className="text-sm text-foreground break-all">{row.url}</p>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
)
|
|
252
|
+
|
|
209
253
|
const drawerToolbarProps = {
|
|
210
254
|
state: tableState,
|
|
211
255
|
totalRows: sites.length,
|
|
@@ -292,6 +336,28 @@ export const SitesTable = React.forwardRef<
|
|
|
292
336
|
)
|
|
293
337
|
}
|
|
294
338
|
|
|
339
|
+
if (view === "panel") {
|
|
340
|
+
return (
|
|
341
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
342
|
+
{sharedToolbar}
|
|
343
|
+
<ListPageSplitHubChrome aria-label="Sites directory panel view">
|
|
344
|
+
<FinderPanelView<SiteDirectoryRow>
|
|
345
|
+
embedded
|
|
346
|
+
groupsColumnTitle="Sites"
|
|
347
|
+
groups={panelGroupsBuilder(tableState.rows)}
|
|
348
|
+
rows={tableState.rows}
|
|
349
|
+
getRowId={(row) => row.id}
|
|
350
|
+
getRowGroupId={() => "all"}
|
|
351
|
+
autoSaveId="sites-panel-view"
|
|
352
|
+
renderListRow={panelRenderListRow}
|
|
353
|
+
renderDetail={panelRenderDetail}
|
|
354
|
+
emptyList={<p className="text-sm text-muted-foreground">No sites found.</p>}
|
|
355
|
+
/>
|
|
356
|
+
</ListPageSplitHubChrome>
|
|
357
|
+
</div>
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
295
361
|
return (
|
|
296
362
|
<div className="flex min-h-0 flex-1 flex-col gap-4">
|
|
297
363
|
{sharedToolbar}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Team roster — DataTable + TablePropertiesDrawer + list/board/dashboard (shared `tableState.rows`).
|
|
4
|
+
* Team roster — DataTable + TablePropertiesDrawer + table/list/board/panel/dashboard (shared `tableState.rows`).
|
|
5
5
|
* Dashboard view uses `TeamDashboardChartsSection` (customise on canvas) + lib/mock/team-kpi.
|
|
6
|
+
* Panel view uses `FinderPanelView` with status-based groups (Active, Away, Invited).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import * as React from "react"
|
|
@@ -27,7 +28,11 @@ import { KEY_METRICS_KPI_COUNT_DEFAULT } from "@/lib/dashboard-layout-merge"
|
|
|
27
28
|
import type { ChartType, DashboardLayout } from "@/components/data-view-dashboard-charts"
|
|
28
29
|
import { TeamListView } from "@/components/team-list-view"
|
|
29
30
|
import { TeamBoardView, TEAM_BOARD_GROUP_OPTIONS } from "@/components/team-board-view"
|
|
31
|
+
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
32
|
+
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
30
33
|
import { teamKpiInsight, teamKpiMetrics } from "@/lib/mock/team-kpi"
|
|
34
|
+
import { useRouter } from "next/navigation"
|
|
35
|
+
import { cn } from "@/lib/utils"
|
|
31
36
|
import type { DataListViewType } from "@/lib/data-list-view"
|
|
32
37
|
import type { OpenTablePropertiesHandle } from "@/lib/list-page-table-properties"
|
|
33
38
|
import type { ColumnDef } from "@/components/data-table/types"
|
|
@@ -67,6 +72,127 @@ const STATUS_FILTER_OPTS = [
|
|
|
67
72
|
{ value: "invited", label: TEAM_MEMBER_STATUS_LABEL.invited },
|
|
68
73
|
]
|
|
69
74
|
|
|
75
|
+
// ─── Team-specific panel view helpers ──────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function TeamFinderListRow({
|
|
78
|
+
member,
|
|
79
|
+
isSelected,
|
|
80
|
+
}: {
|
|
81
|
+
member: TeamMember
|
|
82
|
+
isSelected: boolean
|
|
83
|
+
}) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={`flex w-full min-w-0 items-center gap-3 transition-colors duration-75 ${
|
|
87
|
+
isSelected ? "bg-transparent text-accent-foreground" : "text-foreground"
|
|
88
|
+
}`}
|
|
89
|
+
>
|
|
90
|
+
<AvatarInitials
|
|
91
|
+
initials={member.initials}
|
|
92
|
+
className={cn(
|
|
93
|
+
"size-8 shrink-0 rounded-full text-[11px] font-semibold",
|
|
94
|
+
isSelected ? "ring-2 ring-accent-foreground/35" : "",
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
97
|
+
<div className="min-w-0 flex-1">
|
|
98
|
+
<p className={cn("truncate text-[13px] font-medium leading-tight", isSelected ? "text-accent-foreground" : "text-foreground")}>
|
|
99
|
+
{member.name}
|
|
100
|
+
</p>
|
|
101
|
+
<p className={cn("mt-0.5 truncate text-[11px] leading-tight", isSelected ? "text-accent-foreground/80" : "text-muted-foreground")}>
|
|
102
|
+
{member.role}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
{!isSelected && (
|
|
106
|
+
<ListHubStatusBadge
|
|
107
|
+
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
108
|
+
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
109
|
+
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function TeamFinderDetail({
|
|
117
|
+
member,
|
|
118
|
+
}: {
|
|
119
|
+
member: TeamMember
|
|
120
|
+
}) {
|
|
121
|
+
const router = useRouter()
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
125
|
+
{/* Header */}
|
|
126
|
+
<div className="flex shrink-0 items-start gap-4 border-b border-border px-5 py-4">
|
|
127
|
+
<AvatarInitials initials={member.initials} className="size-14 shrink-0 rounded-full text-lg font-semibold" />
|
|
128
|
+
<div className="min-w-0 flex-1">
|
|
129
|
+
<h2 className="text-base font-semibold text-foreground leading-tight">{member.name}</h2>
|
|
130
|
+
<p className="mt-0.5 text-[13px] text-muted-foreground">{member.role}</p>
|
|
131
|
+
<div className="mt-2">
|
|
132
|
+
<ListHubStatusBadge
|
|
133
|
+
label={TEAM_MEMBER_STATUS_LABEL[member.status]}
|
|
134
|
+
tintClassName={TEAM_MEMBER_STATUS_BADGE_CLASS[member.status]}
|
|
135
|
+
icon={TEAM_MEMBER_STATUS_ICON[member.status]}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<Tip side="bottom" label="Open full profile">
|
|
140
|
+
<Button type="button" variant="outline" size="sm" className="shrink-0"
|
|
141
|
+
onClick={() => router.push(`/team/${member.id}`)}
|
|
142
|
+
aria-label={`Open full profile for ${member.name}`}>
|
|
143
|
+
<i className="fa-light fa-arrow-up-right-from-square text-[12px]" aria-hidden="true" />
|
|
144
|
+
Open
|
|
145
|
+
</Button>
|
|
146
|
+
</Tip>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Fields */}
|
|
150
|
+
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
151
|
+
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
152
|
+
<div className="flex flex-col gap-0.5">
|
|
153
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
154
|
+
<i className="fa-light fa-envelope text-[10px]" aria-hidden="true" /> Email
|
|
155
|
+
</dt>
|
|
156
|
+
<dd className="text-[13px]">
|
|
157
|
+
<a href={`mailto:${member.email}`} className="text-interactive-foreground hover:underline">{member.email}</a>
|
|
158
|
+
</dd>
|
|
159
|
+
</div>
|
|
160
|
+
<div className="flex flex-col gap-0.5">
|
|
161
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
162
|
+
<i className="fa-light fa-briefcase text-[10px]" aria-hidden="true" /> Role
|
|
163
|
+
</dt>
|
|
164
|
+
<dd className="text-[13px] text-foreground">{member.role}</dd>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="flex flex-col gap-0.5">
|
|
167
|
+
<dt className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground flex items-center gap-1.5">
|
|
168
|
+
<i className="fa-light fa-phone text-[10px]" aria-hidden="true" /> Phone
|
|
169
|
+
</dt>
|
|
170
|
+
<dd className="text-[13px] text-foreground">{member.phone}</dd>
|
|
171
|
+
</div>
|
|
172
|
+
</dl>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Status groups for team panel view ────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
const TEAM_STATUS_GROUPS: Array<{ id: string; label: string; accent: string }> = [
|
|
181
|
+
{ id: "all", label: "All", accent: "bg-muted-foreground" },
|
|
182
|
+
{ id: "active", label: "Active", accent: "bg-success" },
|
|
183
|
+
{ id: "away", label: "Away", accent: "bg-warning" },
|
|
184
|
+
{ id: "invited", label: "Invited", accent: "bg-brand" },
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
function buildTeamStatusGroups(members: TeamMember[]): FinderGroup[] {
|
|
188
|
+
return TEAM_STATUS_GROUPS.map(sg => ({
|
|
189
|
+
id: sg.id,
|
|
190
|
+
label: sg.label,
|
|
191
|
+
accent: sg.accent,
|
|
192
|
+
count: sg.id === "all" ? members.length : members.filter(m => m.status === sg.id).length,
|
|
193
|
+
}))
|
|
194
|
+
}
|
|
195
|
+
|
|
70
196
|
function columnToFilterFieldDef(c: ColumnDef<TeamMember>): FilterFieldDef | null {
|
|
71
197
|
if (!c.filter) return null
|
|
72
198
|
const f = c.filter
|
|
@@ -469,6 +595,35 @@ export const TeamTable = React.forwardRef<
|
|
|
469
595
|
)
|
|
470
596
|
}
|
|
471
597
|
|
|
598
|
+
if (view === "panel") {
|
|
599
|
+
const groups = React.useMemo(() => buildTeamStatusGroups(tableState.rows), [tableState.rows])
|
|
600
|
+
return (
|
|
601
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
602
|
+
{sharedToolbar}
|
|
603
|
+
<ListPageSplitHubChrome aria-label="Team members panel view">
|
|
604
|
+
<FinderPanelView<TeamMember>
|
|
605
|
+
embedded
|
|
606
|
+
groupsColumnTitle="Status"
|
|
607
|
+
groups={groups}
|
|
608
|
+
rows={tableState.rows}
|
|
609
|
+
getRowId={r => r.id}
|
|
610
|
+
getRowGroupId={r => r.status}
|
|
611
|
+
defaultGroupId="all"
|
|
612
|
+
autoSaveId="team-panel-view"
|
|
613
|
+
ariaLabel="Team members panel view"
|
|
614
|
+
emptyList={<p>No team members found</p>}
|
|
615
|
+
renderListRow={(member, isSelected) => (
|
|
616
|
+
<TeamFinderListRow member={member} isSelected={isSelected} />
|
|
617
|
+
)}
|
|
618
|
+
renderDetail={member => (
|
|
619
|
+
<TeamFinderDetail member={member} />
|
|
620
|
+
)}
|
|
621
|
+
/>
|
|
622
|
+
</ListPageSplitHubChrome>
|
|
623
|
+
</div>
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
472
627
|
return (
|
|
473
628
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
474
629
|
<CoachMark state={dashboardCustomizeCoach} />
|
|
@@ -478,7 +478,7 @@ export function ListPageTemplate({
|
|
|
478
478
|
<DropdownMenuItem
|
|
479
479
|
key={v.type}
|
|
480
480
|
shortcut={i < 9 ? `⌘⇧${i + 1}` : undefined}
|
|
481
|
-
|
|
481
|
+
onSelect={() => addView(v.type)}
|
|
482
482
|
>
|
|
483
483
|
<i className={`fa-light ${v.icon}`} aria-hidden="true" />
|
|
484
484
|
{v.label}
|
|
@@ -491,7 +491,7 @@ export function ListPageTemplate({
|
|
|
491
491
|
|
|
492
492
|
{/* ── Content — keyed by tab so each view tab owns its height (no stale min-height). ── */}
|
|
493
493
|
{activeTab ? (
|
|
494
|
-
<div key={activeTab.id} className="flex min-h-0 flex-col">
|
|
494
|
+
<div key={activeTab.id} className="flex min-h-0 flex-1 flex-col">
|
|
495
495
|
{renderContent(activeTab, patch => updateTab(activeTab.id, patch))}
|
|
496
496
|
</div>
|
|
497
497
|
) : null}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/avatar"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/badge"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/banner"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/breadcrumb"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/button"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/calendar"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/card"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/chart"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/checkbox"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/coach-mark"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/collapsible"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/command"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/date-picker-field"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/dialog"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/drag-handle-grip"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/drawer"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/dropdown-menu"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/field"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/form"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/input-group"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/input-mask"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/input"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/kbd"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/label"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../../../../packages/ui/src/components/ui/payment-card-fields"
|