@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.
Files changed (139) hide show
  1. package/package.json +2 -1
  2. package/template/.agents/skills/shadcn/SKILL.md +242 -0
  3. package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
  4. package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
  5. package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
  6. package/template/.agents/skills/shadcn/cli.md +257 -0
  7. package/template/.agents/skills/shadcn/customization.md +202 -0
  8. package/template/.agents/skills/shadcn/evals/evals.json +47 -0
  9. package/template/.agents/skills/shadcn/mcp.md +94 -0
  10. package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  11. package/template/.agents/skills/shadcn/rules/composition.md +195 -0
  12. package/template/.agents/skills/shadcn/rules/forms.md +192 -0
  13. package/template/.agents/skills/shadcn/rules/icons.md +101 -0
  14. package/template/.agents/skills/shadcn/rules/styling.md +162 -0
  15. package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
  16. package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
  17. package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
  18. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
  19. package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
  20. package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
  21. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
  22. package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
  23. package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
  24. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
  25. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
  26. package/template/AGENTS.md +52 -11
  27. package/template/app/(app)/dashboard/page.tsx +1 -1
  28. package/template/app/(app)/data-list/[id]/page.tsx +24 -8
  29. package/template/app/(app)/data-list/new/page.tsx +7 -4
  30. package/template/app/(app)/data-list/page.tsx +1 -1
  31. package/template/app/(app)/examples/page.tsx +41 -0
  32. package/template/app/(app)/question-bank/page.tsx +3 -3
  33. package/template/app/globals.css +1 -1
  34. package/template/components/app-sidebar.tsx +52 -35
  35. package/template/components/compliance-table.tsx +79 -0
  36. package/template/components/data-list-client.tsx +36 -25
  37. package/template/components/data-list-table.tsx +797 -10
  38. package/template/components/data-views/finder-panel-view.tsx +405 -0
  39. package/template/components/data-views/folder-grid-view.tsx +86 -0
  40. package/template/components/data-views/index.ts +59 -0
  41. package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  42. package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  43. package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
  44. package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
  45. package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  46. package/template/components/data-views/list-page-view-frame.tsx +53 -0
  47. package/template/components/data-views/os-folder-glyph.tsx +121 -0
  48. package/template/components/folder-details-shell.tsx +230 -0
  49. package/template/components/hub-tree-panel-view.tsx +672 -0
  50. package/template/components/list-hub-status-badge.tsx +17 -3
  51. package/template/components/placements-page-header.tsx +14 -8
  52. package/template/components/placements-table-columns.tsx +8 -8
  53. package/template/components/question-bank-client.tsx +157 -40
  54. package/template/components/question-bank-new-folder-sheet.tsx +248 -0
  55. package/template/components/question-bank-os-folder-view.tsx +648 -0
  56. package/template/components/question-bank-page-header.tsx +3 -3
  57. package/template/components/question-bank-panel-activator.tsx +9 -0
  58. package/template/components/question-bank-secondary-nav.tsx +226 -0
  59. package/template/components/question-bank-table.tsx +707 -22
  60. package/template/components/secondary-panel.tsx +41 -107
  61. package/template/components/sites-table.tsx +66 -0
  62. package/template/components/team-client.tsx +7 -0
  63. package/template/components/team-table.tsx +156 -1
  64. package/template/components/templates/list-page.tsx +2 -2
  65. package/template/components/ui/avatar.tsx +1 -1
  66. package/template/components/ui/badge.tsx +1 -1
  67. package/template/components/ui/banner.tsx +1 -1
  68. package/template/components/ui/breadcrumb.tsx +1 -1
  69. package/template/components/ui/button.tsx +1 -1
  70. package/template/components/ui/calendar.tsx +1 -1
  71. package/template/components/ui/card.tsx +1 -1
  72. package/template/components/ui/chart.tsx +1 -1
  73. package/template/components/ui/checkbox.tsx +1 -1
  74. package/template/components/ui/coach-mark.tsx +1 -1
  75. package/template/components/ui/collapsible.tsx +1 -1
  76. package/template/components/ui/command.tsx +1 -1
  77. package/template/components/ui/date-picker-field.tsx +1 -1
  78. package/template/components/ui/dialog.tsx +1 -1
  79. package/template/components/ui/drag-handle-grip.tsx +1 -1
  80. package/template/components/ui/drawer.tsx +1 -1
  81. package/template/components/ui/dropdown-menu.tsx +1 -1
  82. package/template/components/ui/field.tsx +1 -1
  83. package/template/components/ui/form.tsx +1 -1
  84. package/template/components/ui/input-group.tsx +1 -1
  85. package/template/components/ui/input-mask.tsx +1 -1
  86. package/template/components/ui/input.tsx +1 -1
  87. package/template/components/ui/kbd.tsx +1 -1
  88. package/template/components/ui/label.tsx +1 -1
  89. package/template/components/ui/payment-card-fields.tsx +1 -1
  90. package/template/components/ui/popover.tsx +1 -1
  91. package/template/components/ui/radio-group.tsx +1 -1
  92. package/template/components/ui/resizable.tsx +68 -0
  93. package/template/components/ui/select.tsx +1 -1
  94. package/template/components/ui/selection-tile-grid.tsx +1 -1
  95. package/template/components/ui/separator.tsx +1 -1
  96. package/template/components/ui/sheet.tsx +1 -1
  97. package/template/components/ui/sidebar.tsx +1 -1
  98. package/template/components/ui/skeleton.tsx +1 -1
  99. package/template/components/ui/sonner.tsx +1 -1
  100. package/template/components/ui/status-badge.tsx +1 -1
  101. package/template/components/ui/table.tsx +1 -1
  102. package/template/components/ui/tabs.tsx +1 -1
  103. package/template/components/ui/textarea.tsx +1 -1
  104. package/template/components/ui/tip.tsx +1 -1
  105. package/template/components/ui/toggle-group.tsx +1 -1
  106. package/template/components/ui/toggle-switch.tsx +1 -1
  107. package/template/components/ui/toggle.tsx +1 -1
  108. package/template/components/ui/tooltip.tsx +1 -1
  109. package/template/components/ui/view-segmented-control.tsx +1 -1
  110. package/template/docs/data-views-pattern.md +7 -0
  111. package/template/hooks/use-app-theme.ts +1 -1
  112. package/template/hooks/use-coach-mark.ts +1 -1
  113. package/template/hooks/use-location-hash.ts +15 -0
  114. package/template/hooks/use-mobile.ts +1 -1
  115. package/template/hooks/use-mod-key-label.ts +1 -1
  116. package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
  117. package/template/lib/ask-leo-route-context.ts +25 -57
  118. package/template/lib/coach-mark-registry.ts +13 -13
  119. package/template/lib/command-menu-config.ts +28 -23
  120. package/template/lib/command-menu-search-data.ts +10 -9
  121. package/template/lib/data-list-view-surface.ts +12 -1
  122. package/template/lib/data-list-view.ts +6 -3
  123. package/template/lib/date-filter.ts +1 -1
  124. package/template/lib/mock/dashboard.ts +11 -11
  125. package/template/lib/mock/navigation.tsx +22 -63
  126. package/template/lib/mock/placements-kpi.ts +19 -19
  127. package/template/lib/mock/question-bank-folders.ts +167 -0
  128. package/template/lib/mock/question-bank-inspector.ts +109 -0
  129. package/template/lib/mock/question-bank-kpi.ts +1 -1
  130. package/template/lib/mock/question-bank.ts +80 -0
  131. package/template/lib/question-bank-nav.ts +91 -0
  132. package/template/lib/utils.ts +1 -1
  133. package/template/next.config.mjs +8 -0
  134. package/template/package.json +1 -0
  135. package/template/public/folders/icons8-folder-windows-11.svg +1 -0
  136. package/template/app/(app)/compliance/page.tsx +0 -10
  137. package/template/app/(app)/rotations/page.tsx +0 -15
  138. package/template/app/(app)/sites/all/page.tsx +0 -13
  139. 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: "Placements" }}>
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 { PrimaryPageTemplate } from "@/components/templates/primary-page-template"
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
- <PrimaryPageTemplate siteHeader={{ title: "Question bank" }}>
6
+ <Suspense fallback={null}>
7
7
  <QuestionBankClient />
8
- </PrimaryPageTemplate>
8
+ </Suspense>
9
9
  )
10
10
  }
@@ -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 "../node_modules/@exxatdesignux/ui/src";
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 open={flyoutOpen} onOpenChange={setFlyoutOpen}>
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 open={open} onOpenChange={setOpen} asChild>
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
- Sticky bottom profile + WCAG 1.4.10 Reflow escape hatch.
802
-
803
- Normal viewport:
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
- "[@media(max-height:640px)]:overflow-y-auto",
834
+ reflowZoom && "overflow-y-auto",
821
835
  )}
822
836
  >
823
837
  <SidebarContent
824
838
  className={cn(
825
839
  "gap-0",
826
- // At high zoom, the outer <nav> becomes the scroller — un-flex the
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
- <SidebarGroup className="py-2 border-t border-sidebar-border" role="group" aria-label="Utilities">
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 — Placements page built on the reusable ListPageTemplate.
4
+ * DataListClient — demo list hub on the reusable ListPageTemplate.
5
5
  *
6
- * Uses centralized exports from `@/components/data-views` (same pattern as Team / Compliance).
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 filter your placements by status — All, Upcoming, Ongoing, or Completed. Each view remembers its own settings.",
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", viewType: "table", icon: "fa-table", filterId: "all" },
106
- { id: "upcoming", label: "Upcoming", viewType: "table", icon: "fa-calendar-clock", filterId: "upcoming" },
107
- { id: "ongoing", label: "Ongoing", viewType: "table", icon: "fa-circle-half-stroke", filterId: "ongoing" },
108
- { id: "completed", label: "Completed", viewType: "table", icon: "fa-circle-check", filterId: "completed" },
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: "Upcoming" },
114
- { id: "ongoing", label: "Ongoing" },
115
- { id: "completed", label: "Completed" },
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: "placements-views-tour",
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: "Placements",
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 placements end in the next 30 days?",
152
- "Summarize what’s in this view after my filters",
153
- "What columns should I add for site coordinators?",
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 lifecycle"
216
- getTabCount={(filterId) => placementsForPhase(filterId as PlacementLifecycleTabId).length}
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={tab.filterId as PlacementLifecycleTabId}
234
+ lifecycleTabId={phase}
224
235
  getColumnsForLifecycle={getPlacementColumnsForLifecycle}
225
- emptyTableCopy={emptyCopyForPlacementLifecycleTab(tab.filterId as PlacementLifecycleTabId)}
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
  }