@exxatdesignux/ui 0.2.6 → 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/package.json +2 -1
- 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/placements-page-header.tsx +14 -8
- package/template/components/placements-table-columns.tsx +8 -8
- package/template/components/question-bank-client.tsx +157 -40
- 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 +3 -3
- 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-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
|
@@ -3,7 +3,7 @@ import { PrimaryPageTemplate } from "@/components/templates/primary-page-templat
|
|
|
3
3
|
|
|
4
4
|
export default function DataListPage() {
|
|
5
5
|
return (
|
|
6
|
-
<PrimaryPageTemplate siteHeader={{ title: "
|
|
6
|
+
<PrimaryPageTemplate siteHeader={{ title: "List hub" }}>
|
|
7
7
|
<DataListClient />
|
|
8
8
|
</PrimaryPageTemplate>
|
|
9
9
|
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Link from "next/link"
|
|
2
|
+
import { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
|
|
3
|
+
import { Button } from "@/components/ui/button"
|
|
4
|
+
|
|
5
|
+
const LINKS = [
|
|
6
|
+
{ href: "/dashboard", label: "Dashboard", description: "Metrics, charts, and layout patterns." },
|
|
7
|
+
{ href: "/data-list", label: "List hub", description: "Table, list, board, and dashboard views on shared state." },
|
|
8
|
+
{ href: "/question-bank", label: "Question bank", description: "Folders, OS folder view, panel, and tree demos on mock items." },
|
|
9
|
+
{ href: "/settings", label: "Settings", description: "Appearance, tours, and shell preferences." },
|
|
10
|
+
{ href: "/help", label: "Help", description: "Support and documentation entry points." },
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
export default function ExamplesPage() {
|
|
14
|
+
return (
|
|
15
|
+
<PrimaryPageTemplate
|
|
16
|
+
siteHeader={{ title: "Patterns" }}
|
|
17
|
+
maxWidthClassName="max-w-3xl"
|
|
18
|
+
contentClassName="px-4 lg:px-6 py-8"
|
|
19
|
+
bodyClassName="overflow-y-auto"
|
|
20
|
+
>
|
|
21
|
+
<p className="text-sm text-muted-foreground mb-6">
|
|
22
|
+
This workspace ships neutral chrome so you can reuse layouts, data views, and tokens as a design system.
|
|
23
|
+
</p>
|
|
24
|
+
<ul className="flex flex-col gap-3" role="list">
|
|
25
|
+
{LINKS.map((item) => (
|
|
26
|
+
<li key={item.href}>
|
|
27
|
+
<Button variant="outline" className="h-auto w-full justify-start gap-3 py-4 px-4" asChild>
|
|
28
|
+
<Link href={item.href}>
|
|
29
|
+
<span className="flex min-w-0 flex-1 flex-col items-start gap-0.5 text-left">
|
|
30
|
+
<span className="font-medium text-foreground">{item.label}</span>
|
|
31
|
+
<span className="text-xs font-normal text-muted-foreground">{item.description}</span>
|
|
32
|
+
</span>
|
|
33
|
+
<i className="fa-light fa-arrow-right shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
34
|
+
</Link>
|
|
35
|
+
</Button>
|
|
36
|
+
</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
</PrimaryPageTemplate>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Suspense } from "react"
|
|
2
2
|
import { QuestionBankClient } from "@/components/question-bank-client"
|
|
3
3
|
|
|
4
4
|
export default function QuestionBankPage() {
|
|
5
5
|
return (
|
|
6
|
-
<
|
|
6
|
+
<Suspense fallback={null}>
|
|
7
7
|
<QuestionBankClient />
|
|
8
|
-
</
|
|
8
|
+
</Suspense>
|
|
9
9
|
)
|
|
10
10
|
}
|
package/template/app/globals.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
@import "shadcn/tailwind.css";
|
|
14
14
|
|
|
15
15
|
/* Ensure Tailwind scans the shared UI package for utility classes */
|
|
16
|
-
@source "
|
|
16
|
+
@source "../../../packages/ui/src";
|
|
17
17
|
|
|
18
18
|
/* RTL layout direction support */
|
|
19
19
|
@custom-variant dark (&:is(.dark *));
|
|
@@ -63,6 +63,8 @@ import { Tip } from "@/components/ui/tip"
|
|
|
63
63
|
import { requestOpenCommandMenu } from "@/components/command-menu"
|
|
64
64
|
import { Kbd, KbdGroup } from "@/components/ui/kbd"
|
|
65
65
|
import { useModKeyLabel } from "@/hooks/use-mod-key-label"
|
|
66
|
+
import { useLocationHash } from "@/hooks/use-location-hash"
|
|
67
|
+
import { useSidebarReflowZoom } from "@/hooks/use-sidebar-reflow-zoom"
|
|
66
68
|
import { useProduct, type Product } from "@/contexts/product-context"
|
|
67
69
|
import { NavUser } from "@/components/nav-user"
|
|
68
70
|
import { useSecondaryPanel } from "@/components/secondary-panel"
|
|
@@ -106,17 +108,6 @@ function isNavActive(pathname: string, url: string): boolean {
|
|
|
106
108
|
return pathname.startsWith(`${pathOnly}/`)
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
function useLocationHash(): string {
|
|
110
|
-
const [hash, setHash] = React.useState("")
|
|
111
|
-
React.useEffect(() => {
|
|
112
|
-
const read = () => setHash(typeof window !== "undefined" ? window.location.hash : "")
|
|
113
|
-
read()
|
|
114
|
-
window.addEventListener("hashchange", read)
|
|
115
|
-
return () => window.removeEventListener("hashchange", read)
|
|
116
|
-
}, [])
|
|
117
|
-
return hash
|
|
118
|
-
}
|
|
119
|
-
|
|
120
111
|
/** Sub-item active — catalog detail routes, hash fragments, or duplicate hub URLs (Rotations). */
|
|
121
112
|
function isCollapsibleChildActive(
|
|
122
113
|
pathname: string,
|
|
@@ -217,6 +208,7 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
217
208
|
const isAnyChildActive =
|
|
218
209
|
item.children?.some(c => isCollapsibleChildActive(pathname, item, c, locationHash)) ?? false
|
|
219
210
|
const { state, isMobile } = useSidebar()
|
|
211
|
+
const { openPanel } = useSecondaryPanel()
|
|
220
212
|
const [open, setOpen] = React.useState(isAnyChildActive)
|
|
221
213
|
const [flyoutOpen, setFlyoutOpen] = React.useState(false)
|
|
222
214
|
const flyoutTitleId = React.useId()
|
|
@@ -239,7 +231,15 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
239
231
|
if (iconRailCollapsed) {
|
|
240
232
|
return (
|
|
241
233
|
<SidebarMenuItem>
|
|
242
|
-
<Popover
|
|
234
|
+
<Popover
|
|
235
|
+
open={flyoutOpen}
|
|
236
|
+
onOpenChange={next => {
|
|
237
|
+
setFlyoutOpen(next)
|
|
238
|
+
if (next && item.secondaryPanel) {
|
|
239
|
+
openPanel(item.secondaryPanel)
|
|
240
|
+
}
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
243
|
<Tooltip>
|
|
244
244
|
<TooltipTrigger asChild>
|
|
245
245
|
<PopoverTrigger asChild>
|
|
@@ -305,7 +305,16 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
return (
|
|
308
|
-
<Collapsible
|
|
308
|
+
<Collapsible
|
|
309
|
+
open={open}
|
|
310
|
+
onOpenChange={next => {
|
|
311
|
+
setOpen(next)
|
|
312
|
+
if (next && item.secondaryPanel) {
|
|
313
|
+
openPanel(item.secondaryPanel)
|
|
314
|
+
}
|
|
315
|
+
}}
|
|
316
|
+
asChild
|
|
317
|
+
>
|
|
309
318
|
<SidebarMenuItem>
|
|
310
319
|
<Tooltip>
|
|
311
320
|
<TooltipTrigger asChild>
|
|
@@ -359,6 +368,7 @@ function CollapsibleNavItem({ item, pathname }: { item: NavLinkItem; pathname: s
|
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: string }) {
|
|
371
|
+
const { openPanel } = useSecondaryPanel()
|
|
362
372
|
return (
|
|
363
373
|
<>
|
|
364
374
|
{items.map(item => {
|
|
@@ -370,6 +380,7 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
|
|
|
370
380
|
}
|
|
371
381
|
|
|
372
382
|
const isActive = isNavActive(pathname, item.url)
|
|
383
|
+
const itemPath = navUrlPath(item.url)
|
|
373
384
|
return (
|
|
374
385
|
<SidebarMenuItem key={item.key}>
|
|
375
386
|
<SidebarMenuButton asChild isActive={isActive} tooltip={item.title}>
|
|
@@ -381,6 +392,17 @@ function NavLinkItems({ items, pathname }: { items: NavLinkItem[]; pathname: str
|
|
|
381
392
|
? `${item.title}, ${badgeAccessibleSuffix(item.badge)}`
|
|
382
393
|
: undefined
|
|
383
394
|
}
|
|
395
|
+
onClick={e => {
|
|
396
|
+
if (
|
|
397
|
+
item.secondaryPanel &&
|
|
398
|
+
itemPath &&
|
|
399
|
+
pathname === itemPath &&
|
|
400
|
+
!item.url.includes("#")
|
|
401
|
+
) {
|
|
402
|
+
e.preventDefault()
|
|
403
|
+
openPanel(item.secondaryPanel)
|
|
404
|
+
}
|
|
405
|
+
}}
|
|
384
406
|
>
|
|
385
407
|
<span
|
|
386
408
|
key={isActive ? "active" : "idle"}
|
|
@@ -794,38 +816,28 @@ function SidebarHeaderStack({ children }: { children: React.ReactNode }) {
|
|
|
794
816
|
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
795
817
|
const pathname = usePathname()
|
|
796
818
|
const { isMobile, setOpen } = useSidebar()
|
|
819
|
+
const reflowZoom = useSidebarReflowZoom()
|
|
797
820
|
|
|
798
821
|
return (
|
|
799
822
|
<Sidebar collapsible="icon" {...props}>
|
|
800
823
|
{/*
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
nav → flex column, fills the rail
|
|
805
|
-
content → flex-1 + overflow-auto (scrolls)
|
|
806
|
-
footer → shrink-0 sibling of content (pinned at the bottom)
|
|
807
|
-
|
|
808
|
-
≥ 200 % browser zoom (≈ viewport height ≤ 640 CSS px) — WCAG 1.4.10
|
|
809
|
-
Reflow requires that content stay reachable at high zoom; sticky/pinned
|
|
810
|
-
elements can otherwise eat most of the short viewport and trap users.
|
|
811
|
-
At that breakpoint we make the <nav> itself the single scroll surface
|
|
812
|
-
and un-flex the content, so the footer falls into the natural document
|
|
813
|
-
flow (nothing is sticky anymore and everything scrolls together).
|
|
824
|
+
Normal: scrollable primary rail + sticky bottom block (Settings, Help, profile).
|
|
825
|
+
High zoom / very short viewport (`useSidebarReflowZoom`): single scroll on <nav>
|
|
826
|
+
so nothing is pinned off-screen (WCAG 1.4.10 reflow).
|
|
814
827
|
*/}
|
|
815
828
|
<nav
|
|
816
829
|
aria-label="Application"
|
|
817
830
|
data-exxat-sidebar="application-nav"
|
|
831
|
+
data-reflow-zoom={reflowZoom ? "true" : "false"}
|
|
818
832
|
className={cn(
|
|
819
833
|
"flex min-h-0 flex-1 flex-col",
|
|
820
|
-
"
|
|
834
|
+
reflowZoom && "overflow-y-auto",
|
|
821
835
|
)}
|
|
822
836
|
>
|
|
823
837
|
<SidebarContent
|
|
824
838
|
className={cn(
|
|
825
839
|
"gap-0",
|
|
826
|
-
|
|
827
|
-
// content region so the footer joins the single scroll flow.
|
|
828
|
-
"[@media(max-height:640px)]:!flex-none [@media(max-height:640px)]:!overflow-visible",
|
|
840
|
+
reflowZoom && "!flex-none !overflow-visible",
|
|
829
841
|
)}
|
|
830
842
|
>
|
|
831
843
|
<SidebarHeader className="border-b border-sidebar-border pb-2">
|
|
@@ -882,17 +894,22 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
|
882
894
|
</SidebarGroupContent>
|
|
883
895
|
</SidebarGroup>
|
|
884
896
|
|
|
885
|
-
|
|
897
|
+
</SidebarContent>
|
|
898
|
+
|
|
899
|
+
{/* Settings + Help + profile — pinned under the scrollable rail unless reflow-zoom. */}
|
|
900
|
+
<SidebarFooter
|
|
901
|
+
className={cn(
|
|
902
|
+
"mt-auto border-t border-sidebar-border bg-sidebar",
|
|
903
|
+
reflowZoom && "mt-0 shrink-0",
|
|
904
|
+
)}
|
|
905
|
+
>
|
|
906
|
+
<SidebarGroup className="py-2" role="group" aria-label="Utilities">
|
|
886
907
|
<SidebarGroupContent>
|
|
887
908
|
<SidebarMenu className="gap-0.5">
|
|
888
909
|
<SidebarNavSecondaryItems items={NAV_SECONDARY} pathname={pathname} />
|
|
889
910
|
</SidebarMenu>
|
|
890
911
|
</SidebarGroupContent>
|
|
891
912
|
</SidebarGroup>
|
|
892
|
-
</SidebarContent>
|
|
893
|
-
|
|
894
|
-
{/* Sticky profile — sibling of the scroll area, not a child. */}
|
|
895
|
-
<SidebarFooter className="border-t border-sidebar-border">
|
|
896
913
|
<NavUser user={NAV_USER} />
|
|
897
914
|
</SidebarFooter>
|
|
898
915
|
</nav>
|
|
@@ -44,6 +44,8 @@ import { Tip } from "@/components/ui/tip"
|
|
|
44
44
|
import { CoachMark } from "@/components/ui/coach-mark"
|
|
45
45
|
import { useCoachMark } from "@/hooks/use-coach-mark"
|
|
46
46
|
import { DASHBOARD_CUSTOMIZE_COACH_STEPS } from "@/lib/dashboard-customize-coach-mark"
|
|
47
|
+
import { FinderPanelView, type FinderGroup } from "@/components/data-views/finder-panel-view"
|
|
48
|
+
import { ListPageSplitHubChrome } from "@/components/data-views/list-page-split-hub-chrome"
|
|
47
49
|
import {
|
|
48
50
|
DEFAULT_DATA_LIST_DISPLAY_OPTIONS,
|
|
49
51
|
type DataListDisplayOptions,
|
|
@@ -380,6 +382,61 @@ export const ComplianceTable = React.forwardRef<
|
|
|
380
382
|
? displayOptions.boardGroupByColumnKey
|
|
381
383
|
: "status"
|
|
382
384
|
|
|
385
|
+
// Build panel groups from categories
|
|
386
|
+
const panelGroupsBuilder = (rows: ComplianceItem[]): FinderGroup[] => {
|
|
387
|
+
// Group items by category
|
|
388
|
+
const itemsByCategory = new Map<string, ComplianceItem[]>()
|
|
389
|
+
for (const item of rows) {
|
|
390
|
+
const category = item.category
|
|
391
|
+
if (!itemsByCategory.has(category)) {
|
|
392
|
+
itemsByCategory.set(category, [])
|
|
393
|
+
}
|
|
394
|
+
itemsByCategory.get(category)!.push(item)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Build groups from categories, sorted alphabetically
|
|
398
|
+
const groups: FinderGroup[] = []
|
|
399
|
+
const categories = Array.from(itemsByCategory.keys()).sort()
|
|
400
|
+
|
|
401
|
+
for (const category of categories) {
|
|
402
|
+
const categoryItems = itemsByCategory.get(category) ?? []
|
|
403
|
+
groups.push({
|
|
404
|
+
id: category,
|
|
405
|
+
label: category,
|
|
406
|
+
icon: "fa-folder",
|
|
407
|
+
count: categoryItems.length,
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return groups
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const panelRenderListRow = (row: ComplianceItem, _isSelected: boolean) => (
|
|
415
|
+
<div className="flex-1 min-w-0">
|
|
416
|
+
<p className="text-sm font-medium text-foreground truncate">{row.title}</p>
|
|
417
|
+
<p className="text-xs text-muted-foreground mt-1">{row.category}</p>
|
|
418
|
+
</div>
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
const panelRenderDetail = (row: ComplianceItem) => (
|
|
422
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto p-4">
|
|
423
|
+
<div>
|
|
424
|
+
<h3 className="text-sm font-semibold text-foreground mb-2">Obligation</h3>
|
|
425
|
+
<p className="text-sm text-foreground">{row.title}</p>
|
|
426
|
+
</div>
|
|
427
|
+
<div className="flex flex-col gap-2">
|
|
428
|
+
<div>
|
|
429
|
+
<span className="text-xs font-medium text-muted-foreground">Category</span>
|
|
430
|
+
<p className="text-sm text-foreground">{row.category}</p>
|
|
431
|
+
</div>
|
|
432
|
+
<div>
|
|
433
|
+
<span className="text-xs font-medium text-muted-foreground">Status</span>
|
|
434
|
+
<p className="text-sm text-foreground">{COMPLIANCE_STATUS_LABEL[row.status]}</p>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
)
|
|
439
|
+
|
|
383
440
|
const drawerToolbarProps = {
|
|
384
441
|
totalRows: items.length,
|
|
385
442
|
filterFields,
|
|
@@ -469,6 +526,28 @@ export const ComplianceTable = React.forwardRef<
|
|
|
469
526
|
)
|
|
470
527
|
}
|
|
471
528
|
|
|
529
|
+
if (view === "panel") {
|
|
530
|
+
return (
|
|
531
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
532
|
+
{sharedToolbar}
|
|
533
|
+
<ListPageSplitHubChrome aria-label="Compliance obligations panel view">
|
|
534
|
+
<FinderPanelView<ComplianceItem>
|
|
535
|
+
embedded
|
|
536
|
+
groupsColumnTitle="Category"
|
|
537
|
+
groups={panelGroupsBuilder(tableState.rows)}
|
|
538
|
+
rows={tableState.rows}
|
|
539
|
+
getRowId={(row) => row.id}
|
|
540
|
+
getRowGroupId={(row) => row.category}
|
|
541
|
+
autoSaveId="compliance-panel-view"
|
|
542
|
+
renderListRow={panelRenderListRow}
|
|
543
|
+
renderDetail={panelRenderDetail}
|
|
544
|
+
emptyList={<p className="text-sm text-muted-foreground">No obligations found.</p>}
|
|
545
|
+
/>
|
|
546
|
+
</ListPageSplitHubChrome>
|
|
547
|
+
</div>
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
|
|
472
551
|
return (
|
|
473
552
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
474
553
|
<CoachMark state={dashboardCustomizeCoach} />
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* DataListClient —
|
|
4
|
+
* DataListClient — demo list hub on the reusable ListPageTemplate.
|
|
5
5
|
*
|
|
6
|
-
* Uses centralized exports from `@/components/data-views
|
|
6
|
+
* Uses centralized exports from `@/components/data-views`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as React from "react"
|
|
@@ -36,6 +36,14 @@ import { useAskLeoPageContext } from "@/components/ask-leo-sidebar"
|
|
|
36
36
|
import { CoachMark } from "@/components/ui/coach-mark"
|
|
37
37
|
import { useCoachMark, type CoachMarkStep } from "@/hooks/use-coach-mark"
|
|
38
38
|
|
|
39
|
+
/** Maps each view tab's `filterId` to the demo row segment — unknown ids fall back to all rows. */
|
|
40
|
+
function segmentFilterToPhase(id: string): PlacementLifecycleTabId {
|
|
41
|
+
if (id === "all" || id === "upcoming" || id === "ongoing" || id === "completed") {
|
|
42
|
+
return id
|
|
43
|
+
}
|
|
44
|
+
return "all"
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
48
|
// Coach mark flow — Views & Properties tour
|
|
41
49
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -48,7 +56,7 @@ const VIEWS_TOUR_STEPS: CoachMarkStep[] = [
|
|
|
48
56
|
align: "start",
|
|
49
57
|
title: "Switch Between Views",
|
|
50
58
|
description:
|
|
51
|
-
"Use these tabs to
|
|
59
|
+
"Use these tabs to move between saved segments — All, Due soon, In progress, or Done. Each tab keeps its own layout and properties.",
|
|
52
60
|
},
|
|
53
61
|
{
|
|
54
62
|
id: "views-settings",
|
|
@@ -102,17 +110,17 @@ const VIEWS_TOUR_STEPS: CoachMarkStep[] = [
|
|
|
102
110
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
111
|
|
|
104
112
|
const DEFAULT_TABS: ViewTab[] = [
|
|
105
|
-
{ id: "all", label: "All",
|
|
106
|
-
{ id: "upcoming", label: "
|
|
107
|
-
{ id: "ongoing", label: "
|
|
108
|
-
{ id: "completed", label: "
|
|
113
|
+
{ id: "all", label: "All", viewType: "table", icon: "fa-table", filterId: "all" },
|
|
114
|
+
{ id: "upcoming", label: "Due soon", viewType: "table", icon: "fa-calendar-clock", filterId: "upcoming" },
|
|
115
|
+
{ id: "ongoing", label: "In progress", viewType: "table", icon: "fa-circle-half-stroke", filterId: "ongoing" },
|
|
116
|
+
{ id: "completed", label: "Done", viewType: "table", icon: "fa-circle-check", filterId: "completed" },
|
|
109
117
|
]
|
|
110
118
|
|
|
111
119
|
const LIFECYCLE_OPTIONS = [
|
|
112
|
-
{ id: "all", label: "All"
|
|
113
|
-
{ id: "upcoming", label: "
|
|
114
|
-
{ id: "ongoing", label: "
|
|
115
|
-
{ id: "completed", label: "
|
|
120
|
+
{ id: "all", label: "All" },
|
|
121
|
+
{ id: "upcoming", label: "Due soon" },
|
|
122
|
+
{ id: "ongoing", label: "In progress" },
|
|
123
|
+
{ id: "completed", label: "Done" },
|
|
116
124
|
]
|
|
117
125
|
|
|
118
126
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -130,7 +138,7 @@ export function DataListClient() {
|
|
|
130
138
|
const tableRef = React.useRef<DataListTableHandle>(null)
|
|
131
139
|
|
|
132
140
|
const viewsTour = useCoachMark({
|
|
133
|
-
flowId: "
|
|
141
|
+
flowId: "data-list-views-tour",
|
|
134
142
|
steps: VIEWS_TOUR_STEPS,
|
|
135
143
|
delay: 1200,
|
|
136
144
|
})
|
|
@@ -143,14 +151,14 @@ export function DataListClient() {
|
|
|
143
151
|
useAskLeoPageContext(
|
|
144
152
|
React.useMemo(
|
|
145
153
|
() => ({
|
|
146
|
-
title: "
|
|
154
|
+
title: "List hub",
|
|
147
155
|
description: activeTab
|
|
148
156
|
? `${placementCount} row${placementCount === 1 ? "" : "s"} in “${activeTab.label}” · ${activeTab.viewType} view.`
|
|
149
157
|
: undefined,
|
|
150
158
|
suggestions: [
|
|
151
|
-
"Which
|
|
152
|
-
"Summarize what
|
|
153
|
-
"What columns
|
|
159
|
+
"Which rows are due in the next 30 days?",
|
|
160
|
+
"Summarize what is visible after my filters",
|
|
161
|
+
"What columns help reviewers scan this grid quickly?",
|
|
154
162
|
],
|
|
155
163
|
}),
|
|
156
164
|
[activeTab, placementCount],
|
|
@@ -185,6 +193,7 @@ export function DataListClient() {
|
|
|
185
193
|
return (
|
|
186
194
|
<>
|
|
187
195
|
<CoachMark state={viewsTour} />
|
|
196
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
188
197
|
<ListPageTemplate
|
|
189
198
|
tabs={tabs}
|
|
190
199
|
onTabsChange={setTabs}
|
|
@@ -212,28 +221,30 @@ export function DataListClient() {
|
|
|
212
221
|
showMetrics={showMetrics}
|
|
213
222
|
defaultTabs={DEFAULT_TABS}
|
|
214
223
|
filterOptions={LIFECYCLE_OPTIONS}
|
|
215
|
-
filterLabel="Filter
|
|
216
|
-
getTabCount={(filterId) => placementsForPhase(filterId
|
|
217
|
-
renderContent={(tab, updateTab) =>
|
|
224
|
+
filterLabel="Filter segment"
|
|
225
|
+
getTabCount={(filterId) => placementsForPhase(segmentFilterToPhase(filterId)).length}
|
|
226
|
+
renderContent={(tab, updateTab) => {
|
|
227
|
+
const phase = segmentFilterToPhase(tab.filterId)
|
|
228
|
+
return (
|
|
218
229
|
<DataListTable
|
|
219
230
|
key={tab.id}
|
|
220
231
|
ref={tableRef}
|
|
221
232
|
view={tab.viewType}
|
|
222
233
|
onViewChange={(v: DataListViewType) => updateTab({ viewType: v, icon: dataListViewIcon(v) })}
|
|
223
|
-
lifecycleTabId={
|
|
234
|
+
lifecycleTabId={phase}
|
|
224
235
|
getColumnsForLifecycle={getPlacementColumnsForLifecycle}
|
|
225
|
-
emptyTableCopy={emptyCopyForPlacementLifecycleTab(
|
|
226
|
-
lifecycleDrawerLabel={
|
|
227
|
-
placementLifecycleDrawerLabels[tab.filterId as PlacementLifecycleTabId]
|
|
228
|
-
}
|
|
236
|
+
emptyTableCopy={emptyCopyForPlacementLifecycleTab(phase)}
|
|
237
|
+
lifecycleDrawerLabel={placementLifecycleDrawerLabels[phase]}
|
|
229
238
|
displayOptions={displayOptions}
|
|
230
239
|
onDisplayOptionsChange={patch =>
|
|
231
240
|
setDisplayOptions(prev => ({ ...prev, ...patch }))}
|
|
232
241
|
/>
|
|
233
|
-
|
|
242
|
+
)
|
|
243
|
+
}}
|
|
234
244
|
exportOpen={exportOpen}
|
|
235
245
|
onExportOpenChange={setExportOpen}
|
|
236
246
|
/>
|
|
247
|
+
</div>
|
|
237
248
|
</>
|
|
238
249
|
)
|
|
239
250
|
}
|