@exxatdesignux/ui 0.2.9 → 0.2.11

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 (126) hide show
  1. package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
  2. package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
  3. package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
  4. package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
  5. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
  6. package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
  7. package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
  8. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
  9. package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
  10. package/consumer-extras/patterns/data-views-pattern.md +12 -4
  11. package/package.json +4 -1
  12. package/src/components/ui/banner.tsx +20 -7
  13. package/src/components/ui/date-picker-field.tsx +3 -3
  14. package/src/components/ui/dropdown-menu.tsx +17 -6
  15. package/src/components/ui/input-group.tsx +1 -1
  16. package/src/components/ui/input.tsx +1 -1
  17. package/src/components/ui/select.tsx +1 -1
  18. package/src/components/ui/separator.tsx +2 -2
  19. package/src/components/ui/sidebar.tsx +31 -3
  20. package/src/components/ui/textarea.tsx +1 -1
  21. package/src/globals.css +0 -1
  22. package/src/index.ts +1 -0
  23. package/src/lib/date-filter.ts +13 -4
  24. package/src/lib/dropdown-menu-surface.ts +13 -0
  25. package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
  26. package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
  27. package/template/.nvmrc +1 -1
  28. package/template/AGENTS.md +82 -27
  29. package/template/app/(app)/examples/page.tsx +2 -1
  30. package/template/app/(app)/help/page.tsx +6 -0
  31. package/template/app/(app)/layout.tsx +7 -4
  32. package/template/app/(app)/question-bank/find/page.tsx +12 -0
  33. package/template/app/(app)/question-bank/layout.tsx +46 -0
  34. package/template/app/(app)/question-bank/library/page.tsx +11 -0
  35. package/template/app/(app)/question-bank/list/page.tsx +12 -0
  36. package/template/app/(app)/question-bank/page.tsx +4 -3
  37. package/template/app/globals.css +1 -2
  38. package/template/components/app-sidebar.tsx +51 -13
  39. package/template/components/ask-leo-composer.tsx +173 -45
  40. package/template/components/ask-leo-sidebar.tsx +9 -1
  41. package/template/components/chart-area-interactive.tsx +3 -13
  42. package/template/components/charts-overview.tsx +33 -6
  43. package/template/components/collaboration-access-flow.tsx +144 -0
  44. package/template/components/compliance-page-header.tsx +1 -1
  45. package/template/components/compliance-table.tsx +2 -2
  46. package/template/components/dashboard-tabs.tsx +4 -3
  47. package/template/components/data-list-table-cells.tsx +1 -1
  48. package/template/components/data-list-table.tsx +1 -1
  49. package/template/components/data-table/index.tsx +5 -5
  50. package/template/components/data-table/use-table-state.ts +18 -2
  51. package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
  52. package/template/components/data-view-dashboard-charts-team.tsx +8 -5
  53. package/template/components/data-view-dashboard-charts.tsx +62 -227
  54. package/template/components/dedicated-search-recents.tsx +96 -0
  55. package/template/components/dedicated-search-url-composer.tsx +112 -0
  56. package/template/components/getting-started.tsx +1 -1
  57. package/template/components/hub-tree-panel-view.tsx +10 -26
  58. package/template/components/invite-collaborators-drawer.tsx +453 -0
  59. package/template/components/key-metrics.tsx +54 -8
  60. package/template/components/nav-documents.tsx +1 -1
  61. package/template/components/new-placement-form.tsx +3 -3
  62. package/template/components/page-header.tsx +76 -59
  63. package/template/components/placements-board-view.tsx +3 -3
  64. package/template/components/placements-page-header.tsx +1 -1
  65. package/template/components/placements-table-columns.tsx +3 -2
  66. package/template/components/product-switcher.tsx +0 -1
  67. package/template/components/question-bank-board-view.tsx +35 -47
  68. package/template/components/question-bank-client.tsx +293 -81
  69. package/template/components/question-bank-dashboard-charts.tsx +174 -0
  70. package/template/components/question-bank-favorite-button.tsx +46 -0
  71. package/template/components/question-bank-hub-client.tsx +436 -0
  72. package/template/components/question-bank-list-view.tsx +26 -19
  73. package/template/components/question-bank-new-folder-sheet.tsx +56 -42
  74. package/template/components/question-bank-os-folder-view.tsx +3 -14
  75. package/template/components/question-bank-page-header.tsx +85 -53
  76. package/template/components/question-bank-panel-activator.tsx +3 -4
  77. package/template/components/question-bank-secondary-nav.tsx +523 -65
  78. package/template/components/question-bank-table.tsx +125 -343
  79. package/template/components/secondary-panel.tsx +130 -63
  80. package/template/components/settings-client.tsx +3 -1
  81. package/template/components/sidebar-shell.tsx +2 -0
  82. package/template/components/sites-all-client.tsx +1 -1
  83. package/template/components/sites-table.tsx +1 -1
  84. package/template/components/system-banner-slot.tsx +2 -1
  85. package/template/components/table-properties/drawer.tsx +3 -3
  86. package/template/components/table-properties/sort-card.tsx +1 -1
  87. package/template/components/team-page-header.tsx +1 -1
  88. package/template/components/team-table.tsx +8 -4
  89. package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
  90. package/template/components/templates/dedicated-search-results-template.tsx +19 -0
  91. package/template/components/templates/discovery-hub-template.tsx +273 -0
  92. package/template/components/templates/list-page.tsx +11 -4
  93. package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
  94. package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
  95. package/template/docs/card-vs-rows-pattern.md +36 -0
  96. package/template/docs/collaboration-access-pattern.md +114 -0
  97. package/template/docs/data-views-pattern.md +12 -4
  98. package/template/docs/drawer-vs-dialog-pattern.md +50 -0
  99. package/template/docs/kpi-strip-max-four-pattern.md +29 -0
  100. package/template/docs/kpi-trend-pattern.md +43 -0
  101. package/template/fontawesome-subset.manifest.json +2 -2
  102. package/template/hooks/use-location-hash.ts +14 -8
  103. package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
  104. package/template/lib/ask-leo-route-context.ts +24 -0
  105. package/template/lib/collaborator-access.ts +92 -0
  106. package/template/lib/command-menu-config.ts +8 -1
  107. package/template/lib/command-menu-search-data.ts +11 -8
  108. package/template/lib/data-list-display-options.ts +1 -1
  109. package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
  110. package/template/lib/date-filter.ts +1 -0
  111. package/template/lib/dedicated-search-recents.ts +76 -0
  112. package/template/lib/dedicated-search-url.ts +23 -0
  113. package/template/lib/discovery-hub.ts +15 -0
  114. package/template/lib/list-status-badges.ts +1 -21
  115. package/template/lib/mock/navigation.tsx +4 -2
  116. package/template/lib/mock/placements.ts +9 -9
  117. package/template/lib/mock/question-bank-folders.ts +7 -0
  118. package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
  119. package/template/lib/mock/question-bank-inspector.ts +1 -2
  120. package/template/lib/mock/question-bank-kpi.ts +38 -26
  121. package/template/lib/mock/question-bank.ts +43 -16
  122. package/template/lib/question-bank-dedicated-search.ts +19 -0
  123. package/template/lib/question-bank-hub-search.ts +90 -0
  124. package/template/lib/question-bank-nav.ts +322 -6
  125. package/template/lib/question-bank-recent-searches.ts +22 -0
  126. package/template/package.json +1 -2
@@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
10
10
  type={type}
11
11
  data-slot="input"
12
12
  className={cn(
13
- "h-8 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/15 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
13
+ "h-8 w-full min-w-0 rounded-md border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
14
14
  className
15
15
  )}
16
16
  {...props}
@@ -111,7 +111,7 @@ function SelectItem({
111
111
  <SelectPrimitive.Item
112
112
  data-slot="select-item"
113
113
  className={cn(
114
- "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
114
+ "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pe-8 ps-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_i]:pointer-events-none [&_svg]:shrink-0 [&_i]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
115
115
  className
116
116
  )}
117
117
  {...props}
@@ -17,8 +17,8 @@ function Separator({
17
17
  decorative={decorative}
18
18
  orientation={orientation}
19
19
  className={cn(
20
- "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
21
- className
20
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:self-stretch",
21
+ className,
22
22
  )}
23
23
  {...props}
24
24
  />
@@ -18,6 +18,8 @@ import {
18
18
 
19
19
  const SIDEBAR_COOKIE_NAME = "sidebar_state"
20
20
  const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
21
+ /** Matches `useIsMobile` / Tailwind `md:` — do not apply desktop cookie to mobile overlay. */
22
+ const SIDEBAR_COOKIE_VIEWPORT_MQ = "(max-width: 767px)"
21
23
  const SIDEBAR_WIDTH = "16rem"
22
24
  const SIDEBAR_WIDTH_ICON = "3rem"
23
25
  const SIDEBAR_KEYBOARD_SHORTCUT = "b"
@@ -43,6 +45,13 @@ function useSidebar() {
43
45
  return context
44
46
  }
45
47
 
48
+ function readSidebarStateCookie(): boolean | undefined {
49
+ if (typeof document === "undefined") return undefined
50
+ const m = document.cookie.match(new RegExp(`(?:^|; )${SIDEBAR_COOKIE_NAME}=(true|false)(?:;|$)`))
51
+ if (!m) return undefined
52
+ return m[1] === "true"
53
+ }
54
+
46
55
  function SidebarProvider({
47
56
  defaultOpen = true,
48
57
  open: openProp,
@@ -63,6 +72,18 @@ function SidebarProvider({
63
72
  // We use openProp and setOpenProp for control from outside the component.
64
73
  const [_open, _setOpen] = React.useState(defaultOpen)
65
74
  const open = openProp ?? _open
75
+
76
+ // `setOpen` already persists `sidebar_state` to a cookie on desktop; restore it on mount so
77
+ // full reloads and new tabs keep the rail expanded/collapsed. Skip when controlled or on mobile.
78
+ React.useLayoutEffect(() => {
79
+ if (openProp !== undefined) return
80
+ if (typeof window === "undefined") return
81
+ if (window.matchMedia(SIDEBAR_COOKIE_VIEWPORT_MQ).matches) return
82
+ const fromCookie = readSidebarStateCookie()
83
+ if (fromCookie === undefined) return
84
+ _setOpen(fromCookie)
85
+ }, [openProp])
86
+
66
87
  const setOpen = React.useCallback(
67
88
  (value: boolean | ((value: boolean) => boolean)) => {
68
89
  const openState = typeof value === "function" ? value(open) : value
@@ -530,8 +551,15 @@ const SidebarMenuButton = React.forwardRef<
530
551
  data-slot="sidebar-menu-button"
531
552
  data-sidebar="menu-button"
532
553
  data-size={size}
533
- data-active={isActive}
534
- className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
554
+ data-active={isActive || undefined}
555
+ className={cn(
556
+ sidebarMenuButtonVariants({ variant, size }),
557
+ className,
558
+ // `asChild` merges the child (e.g. Next `<Link className="w-full">`) onto this node —
559
+ // plain `w-full` can win over `group-data-[collapsible=icon]:w-8` in the stylesheet and
560
+ // squash the icon rail to a non-square hit target (WCAG 2.5.8).
561
+ "group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!min-w-8 group-data-[collapsible=icon]:!max-w-8 group-data-[collapsible=icon]:shrink-0",
562
+ )}
535
563
  {...props}
536
564
  />
537
565
  )
@@ -683,7 +711,7 @@ function SidebarMenuSubButton({
683
711
  data-slot="sidebar-menu-sub-button"
684
712
  data-sidebar="menu-sub-button"
685
713
  data-size={size}
686
- data-active={isActive}
714
+ data-active={isActive || undefined}
687
715
  className={cn(
688
716
  "flex h-7 min-w-0 cursor-pointer select-none -translate-x-px rtl:translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-sm data-[size=sm]:text-xs data-active:bg-background data-active:text-foreground data-active:shadow-sm data-active:ring-1 data-active:ring-sidebar-border data-active:hc:border data-active:hc:border-foreground data-active:forced-colors:border data-active:forced-colors:border-[Highlight] [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
689
717
  className
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/15 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
10
+ "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
11
  className
12
12
  )}
13
13
  {...props}
package/src/globals.css CHANGED
@@ -10,7 +10,6 @@
10
10
 
11
11
  @import "tailwindcss";
12
12
  @import "tw-animate-css";
13
- @import "shadcn/tailwind.css";
14
13
  /* Inter is loaded via next/font/google in layout.tsx — no @import needed here */
15
14
 
16
15
  /* RTL layout direction support */
package/src/index.ts CHANGED
@@ -51,5 +51,6 @@ export * from "./hooks/use-mobile"
51
51
  export * from "./hooks/use-mod-key-label"
52
52
 
53
53
  // Utilities
54
+ export * from "./lib/dropdown-menu-surface"
54
55
  export * from "./lib/utils"
55
56
  export * from "./lib/date-filter"
@@ -15,6 +15,15 @@ export function formatDateUS(raw: string | null | undefined): string {
15
15
  return `${m}/${day}/${y}`
16
16
  }
17
17
 
18
+ /** Format a `Date` with local calendar fields as MM/DD/YYYY (avoids UTC drift from `toISOString()`). */
19
+ export function formatDateFromDate(raw: Date | null | undefined): string {
20
+ if (!raw || Number.isNaN(raw.getTime())) return "—"
21
+ const m = String(raw.getMonth() + 1).padStart(2, "0")
22
+ const day = String(raw.getDate()).padStart(2, "0")
23
+ const y = raw.getFullYear()
24
+ return `${m}/${day}/${y}`
25
+ }
26
+
18
27
  /**
19
28
  * Format a Date (or ISO string) into "MM/DD/YYYY hh:mm AM/PM EST".
20
29
  * Time zone label is always appended as the literal string "EST" (display only).
@@ -45,11 +54,11 @@ export function parseRowDateToYmd(raw: string): string | null {
45
54
  return `${y}-${m}-${day}`
46
55
  }
47
56
 
48
- /** Format YYYY-MM-DD for compact filter chip label. */
57
+ /** Format YYYY-MM-DD for filter chip labels (MM/DD/YYYY). */
49
58
  export function formatYmdForDisplay(ymd: string): string {
50
- const d = new Date(`${ymd}T12:00:00`)
51
- if (Number.isNaN(d.getTime())) return ymd
52
- return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })
59
+ const d = ymdToLocalDate(ymd)
60
+ if (!d) return ymd
61
+ return formatDateFromDate(d)
53
62
  }
54
63
 
55
64
  /** Local noon to avoid timezone shifting the calendar day. */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Default surface sizing for product dropdown menus (view settings, row ⋯, column menus, etc.).
3
+ *
4
+ * Uses **pure CSS** (`w-max` + `min-w-*` + `max-w-*`) so width follows labels and shortcuts
5
+ * without **ResizeObserver** or layout thrash.
6
+ *
7
+ * Override when you need a fixed rail, for example:
8
+ * - `className="w-20"` — page-size picker in `DataTablePaginated`
9
+ * - `className="w-(--radix-dropdown-menu-trigger-width) min-w-60"` — account / identity menus
10
+ * - `className="!w-max min-w-72 …"` — very wide school/program switcher
11
+ */
12
+ export const DROPDOWN_MENU_CONTENT_SURFACE_CLASS =
13
+ "min-w-52 w-max max-w-[min(24rem,calc(100vw-2rem))]" as const
@@ -21,6 +21,7 @@ description: >
21
21
  - **Stack:** Next.js 16 (App Router), React, TypeScript, Tailwind CSS, shadcn/ui primitives, Font Awesome icons
22
22
  - **App root:** `exxat-ds/app/(app)/` — route group that wraps all authenticated pages
23
23
  - **Single source of truth:** `exxat-ds/AGENTS.md` for full prose explanations; this skill is the actionable summary
24
+ - **Companion skills (narrow topics):** `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-dedicated-search-surfaces`, `exxat-accessibility`, `exxat-board-cards`, `exxat-collaboration-access` — live under `.cursor/skills/`; vetted copies ship with **`@exxatdesignux/ui`** in `consumer-extras/cursor-skills/` after **`pnpm --filter @exxatdesignux/ui vendor:consumer-extras`**.
24
25
 
25
26
  ---
26
27
 
@@ -87,7 +88,7 @@ To add a primary nav item, append to `NAV_PRIMARY`:
87
88
  | Concern | Pattern |
88
89
  |--------|---------|
89
90
  | **Product (One / Prism)** | **`ExxatProductLogo`** (`components/exxat-product-logo.tsx`) for the header control and **`ProductSwitcher`** — **not** logo.dev rasters unless product explicitly changes that. |
90
- | **School/program menu width** | **`DropdownMenuContent`** ships with **`w-(--radix-dropdown-menu-trigger-width)`**, so the panel matches the **narrow sidebar trigger** and long names wrap too early. For **`TeamSwitcher`**, override with e.g. **`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**. |
91
+ | **School/program menu width** | **`DropdownMenuContent`** defaults to **intrinsic width** (**`min-w-52 w-max max-w-[min(24rem,calc(100vw-2rem))]`** via **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** in **`@exxatdesignux/ui/lib/dropdown-menu-surface`**) pure CSS, no **`ResizeObserver`**. The **school / program** switcher still uses an explicit wider surface (**`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) so dense rows stay readable. |
91
92
  | **School/program copy** | **Do not truncate** school or program names in the switcher; wrap (**`break-words`**, **`whitespace-normal`**, **`items-start`** on multi-line rows). The selected-school summary shows **school name + current program**. |
92
93
  | **Team switcher trigger** | **`SidebarMenuButton` `size="lg"`** uses **`h-12`** + **`overflow-hidden`**, which **clips** a second line (program). When the sidebar is **expanded** or **mobile**, add **`h-auto min-h-12`** and **`overflow-x-clip overflow-y-visible`**. On **icon rail**, hide label rows with **`group-data-[collapsible=icon]:hidden`** (tooltip still exposes the full string). Icon mode defaults **`size-8` + `p-2`** (~16px inner) **clips** school logos — override **`!size-9`**, **`!p-0`**, **`overflow-visible`**. Omit header **chevrons** next to logos if they look like stray chrome. |
93
94
  | **Motion / Animate UI** | [Animate UI](https://animate-ui.com/docs) — open **copy-first** animated components (Motion + Tailwind). This repo uses **`motion/react`** + **`lib/motion-ui.ts`** presets; pull more animations from their registry into `components/` when needed. |
@@ -178,6 +179,17 @@ Align with **`exxat-ds/AGENTS.md` §6.4**, **`docs/data-views-pattern.md`**, **`
178
179
  - `select` column: `defaultPin: "left"`, `lockPin: true`
179
180
  - `actions` column: `defaultPin: "right"`, `lockPin: true`
180
181
 
182
+ ### 5.1 Data table and view-toolbar menus
183
+
184
+ **`DropdownMenuContent`** (from **`@/components/ui/dropdown-menu`**, backed by **`@exxatdesignux/ui`**) applies a **default surface** so **view settings**, **Add view**, **row ⋯**, **column ⋯**, and **filter field** menus get **`min-w-52`**, grow with **`w-max`**, and cap at **`max-w-[min(24rem,calc(100vw-2rem))]`** — all **static Tailwind** (no **`ResizeObserver`** / layout measurement).
185
+
186
+ - **Override** only when the UX needs a fixed rail (e.g. **`className="w-20"`** on the pagination page-size menu, **`w-(--radix-dropdown-menu-trigger-width) min-w-60`** on **`NavUser`**, **`!w-max min-w-72 …`** on the school/program switcher).
187
+ - **Reuse** **`DROPDOWN_MENU_CONTENT_SURFACE_CLASS`** if you build a custom menu primitive that does not wrap **`DropdownMenuContent`**.
188
+
189
+ ### 5.2 KPI trends (`KeyMetrics`, `*-kpi.ts`)
190
+
191
+ **`MetricItem.trend`** must match the **signed change** (arrow direction = truth). **`trendPolarity`** (`higher_is_better` default, **`lower_is_better`**, **`informational`**) controls **tints** and **`aria-label`** — e.g. **low PBI / review flags** rising → `trend: "up"` + **`lower_is_better`** → unfavourable (red), not green. **Doc:** **`docs/kpi-trend-pattern.md`** · **Rule:** **`.cursor/rules/exxat-kpi-trends.mdc`** · **Skill:** **`.cursor/skills/exxat-kpi-trends/SKILL.md`**.
192
+
181
193
  **DataTable must wrap in `<div className="pb-6">`.**
182
194
 
183
195
  ---
@@ -206,7 +218,7 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
206
218
  </Button>
207
219
  </DropdownMenuTrigger>
208
220
  </Tip>
209
- <DropdownMenuContent align="end" className="w-52">
221
+ <DropdownMenuContent align="end">
210
222
  <DropdownMenuItem onClick={onExport}>
211
223
  <i className="fa-light fa-arrow-down-to-line" aria-hidden="true" />
212
224
  Export
@@ -229,6 +241,12 @@ Use `PageHeader` from `@/components/page-header` for the content-area header (be
229
241
  - Subtitle: `"{count} items · Last updated now"` format
230
242
  - Title uses Ivy Presto (`font-heading` variable) — applied automatically by `PageHeader`
231
243
 
244
+ ### 6.1 Collaboration & access (shared hubs)
245
+
246
+ When a hub is **shared**, use **`PageHeader` `variant="collaboration"`**: **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). **Invite people** also lives under the entity header **⋯ More** and opens **`InviteCollaboratorsDrawer`** via **`CollaborationAccessFlow`** when possible. Library access (Owner / Editor / Commenter / Viewer) comes from **`lib/collaborator-access.ts`**; directory tags (Faculty, Program coordinator, Director) use **`PageHeaderCollaborator.roles`**.
247
+
248
+ **Handbook:** `apps/web/AGENTS.md` §4.7 · **Doc:** `docs/collaboration-access-pattern.md` · **Skill:** `.cursor/skills/exxat-collaboration-access/SKILL.md` · **Reference:** Question bank header + client.
249
+
232
250
  ---
233
251
 
234
252
  ## 7. Navigation: Breadcrumbs vs Back Link
@@ -449,9 +467,9 @@ Full checklist in `references/accessibility.md`. Summary of the most-violated ru
449
467
 
450
468
  ### Icons that communicate information — always have a text alternative
451
469
 
452
- This rule covers **every icon that carries meaning**, not only icon-only buttons. FA glyphs, inline SVGs, trend arrows, status dots, chart-legend squares, calendar/clock/pin icons in cells — if the icon tells the user something, that something MUST be reachable by screen readers AND discoverable to sighted users who don't recognise the glyph. SC 1.1.1, 3.3.2, 2.4.6.
470
+ This rule covers **every icon that carries meaning**, not only icon-only buttons. FA glyphs, inline SVGs, avatar placeholders, trend arrows, status dots, chart-legend squares, calendar/clock/pin icons in cells — if the icon **tells the user something**, that something MUST be reachable by screen readers AND discoverable to sighted users who don't recognise the glyph. SC 1.1.1, 3.3.2, 2.4.6.
453
471
 
454
- **Case A — Decorative icon next to text that already names it** → icon is `aria-hidden`, no tooltip.
472
+ **Case A — Decorative icon next to text that already names it** → icon is `aria-hidden`, no `aria-label`, no tooltip. The text is the alt.
455
473
 
456
474
  ```tsx
457
475
  <span className="flex items-center gap-1.5">
@@ -460,7 +478,7 @@ This rule covers **every icon that carries meaning**, not only icon-only buttons
460
478
  </span>
461
479
  ```
462
480
 
463
- **Case B — Informational icon standing alone** (calendar = "date range", clock = "updated at", pin = "site", trend arrow, status dot, icon-only chart legend) → MUST pair `role="img"` + `aria-label` with a visible `Tooltip`. Wrapper MUST be keyboard-focusable (`tabIndex={0}`).
481
+ **Case B — Informational icon standing alone** (calendar = "date range", clock = "updated at", pin = "site", cap = "student", trend arrow, status dot, icon-only chart legend) → MUST pair `role="img"` + `aria-label` with a visible `Tooltip`. Wrapper MUST be keyboard-focusable (`tabIndex={0}`).
464
482
 
465
483
  ```tsx
466
484
  <Tooltip>
@@ -474,7 +492,7 @@ This rule covers **every icon that carries meaning**, not only icon-only buttons
474
492
  </Tooltip>
475
493
  ```
476
494
 
477
- **Case C — Interactive icon-only button / link** → MUST pair `aria-label` on the `<button>` with a wrapping `Tooltip`. Inner `<i>` / `<svg>` is `aria-hidden`. Target ≥ 24×24.
495
+ **Case C — Interactive icon-only button / link** (close `×`, chevron, overflow `⋯`, sort, filter-dismiss, copy, Ask Leo toggle, row actions) → MUST pair `aria-label` on the `<button>` with a wrapping `Tooltip`. Inner `<i>` / `<svg>` is `aria-hidden`. Target ≥ 24×24.
478
496
 
479
497
  ```tsx
480
498
  <Tooltip>
@@ -490,7 +508,7 @@ This rule covers **every icon that carries meaning**, not only icon-only buttons
490
508
  </Tooltip>
491
509
  ```
492
510
 
493
- **Decision tree:** adjacent text label? → A. Else interactive? → C. Else → B. When in doubt: add the accessible name + tooltip.
511
+ **Decision tree:** adjacent text label? → A. Else interactive? → C. Else → B. When in doubt: add the accessible name + tooltip. Narrow exception for all cases: a chevron inside a labelled composite (`Select`, `Combobox`) where the parent already carries the name.
494
512
 
495
513
  ### Touch targets
496
514
  - Minimum **24×24 CSS px** for all interactive controls
@@ -696,8 +714,8 @@ Copy and complete for every list/table/hub page:
696
714
  - [ ] All dates: `MM/DD/YYYY` / `MM/DD/YYYY hh:mm AM/PM EST`
697
715
  - [ ] All tooltips via `<Tip>` — no `title` attribute
698
716
  - [ ] All icons: `aria-hidden="true"`; Ask Leo: `fa-duotone fa-solid fa-star-christmas text-brand`
699
- - [ ] **Every icon that communicates info has a text alternative** — Case A adjacent label, Case B `role="img"` + `aria-label` + `Tooltip`, Case C `aria-label` + `Tooltip` on icon-only buttons; target ≥ 24×24 px. See §12 *Icons that communicate information*.
700
- - [ ] **`Kbd` inside a `Button` uses `variant="bare"`**; **`Kbd` inside `TooltipContent` uses the default tile** — see §11 Keyboard shortcuts
717
+ - [ ] **Every icon that communicates info has a text alternative** — Case A adjacent label (preferred), Case B `role="img"` + `aria-label` + `Tooltip` (calendar-for-date, status dot, trend arrow, icon-only legend), Case C `aria-label` + `Tooltip` on icon-only buttons; target ≥ 24×24 px. See §12 *Icons that communicate information*.
718
+ - [ ] **`Kbd` inside a `Button` uses `variant="bare"`** (glue chords into one bare kbd); **`Kbd` inside `TooltipContent` uses the default tile** — see §11 Keyboard shortcuts
701
719
  - [ ] `DialogTitle`/`SheetTitle`/`DrawerTitle` present on every overlay
702
720
  - [ ] `role="tablist"` contains only tab-role children
703
721
  - [ ] No new shadcn components, no hardcoded colors, no duplicate component abstractions
@@ -14,6 +14,7 @@ For **any app screen that shows a browsable, filterable grid of records** (lists
14
14
  3. **Filters:** Use the shared filter model (filter chips / `FilterFieldDef` and operators) consistent with existing list pages — not one-off filter UIs that bypass the table stack.
15
15
  4. **Table properties:** Include **Table properties** via `TablePropertiesDrawer` from `@/components/table-properties/drawer` (or the same toolbar + drawer pattern used on reference pages such as placements / data list). Users must be able to adjust columns, density, and related table settings from one place.
16
16
  5. **Active view:** On **`ListPageTemplate`** pages with **table / list / board / dashboard** tabs, **`TablePropertiesDrawer`** **MUST** receive **`currentView`** and **`onViewChange`** (see **`./AGENTS.md` §4.2** and **`.cursor/rules/exxat-table-properties-drawer.mdc`**) so Properties matches the selected view (not table-only copy on Board).
17
+ 6. **Dropdown menus:** **`DropdownMenuContent`** uses the shared **`@exxatdesignux/ui`** default (**intrinsic `w-max`**, **`min-w-52`**, capped **`max-w`**) for view settings, row ⋯, column menus, and filter pickers — **pure CSS**, no **`ResizeObserver`**. Override only for deliberate narrow/wide rails (e.g. pagination **`w-20`**, account trigger-width, school switcher **`!w-max min-w-72 …`**). See **`docs/data-views-pattern.md`** (“Dropdown menus”).
17
18
 
18
19
  **Reference implementation:** `components/data-list-table.tsx` (placements) shows how `DataTable`, filters, and `TablePropertiesDrawer` compose together.
19
20
 
package/template/.nvmrc CHANGED
@@ -1 +1 @@
1
- 24
1
+ 22
@@ -10,7 +10,7 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
10
10
 
11
11
  ## 1. How to use this file (for AI agents)
12
12
 
13
- 1. **Before** adding or changing a **list, table, board, or data-heavy page**, read **§3–§6** (including **§6.4** page vs drawer when scoping flows) and run the **§13 checklist**.
13
+ 1. **Before** adding or changing a **list, table, board, or data-heavy page**, read **§3–§6** (including **§6.4** drawer vs dialog vs route when scoping overlays and flows) and run the **§13 checklist**.
14
14
  2. **Before** changing **keyboard hints or shortcuts**, read **§7** and root `.cursor/rules/exxat-kbd-shortcuts.mdc`.
15
15
  3. **Before** changing **table behavior**, read **§3** and root `.cursor/rules/exxat-data-tables.mdc`. **Before** wiring **`TablePropertiesDrawer`** on **`ListPageTemplate`** (view tabs), read **§4.2** and **`.cursor/rules/exxat-table-properties-drawer.mdc`**.
16
16
  4. **Before** building or changing **tabs, nav, dialogs, icon-only controls, or color/contrast**, read **§8** and **`.cursor/skills/exxat-accessibility/SKILL.md`** (from monorepo root when the parent repo is open).
@@ -19,17 +19,22 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
19
19
  7. **Before** adding **folder, panel, or other non-table view bodies** (centered grids, reusable shells), read **§4.5** and **`.cursor/rules/exxat-list-page-view-shells.mdc`** / **`.cursor/skills/exxat-list-page-view-shells/SKILL.md`**.
20
20
  8. **Before** adding or changing **Font Awesome** icons in app UI, read **`.cursor/rules/exxat-fontawesome-icons.mdc`** (Kit subsetting, weights, **`aria-hidden`** on **`<i>`**).
21
21
  9. **Before** adding a **primary nav row** that opens a **nested secondary nav panel** (Question bank style), read **§4.6** and **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
22
- 10. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
23
- 11. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
24
- 12. **Before** choosing **drawer vs new page** for a task flow, read **§6.4** and **`docs/data-views-pattern.md`** (Page vs drawer).
25
- 13. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
26
- 14. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
22
+ 10. **Before** adding **shared access / invite collaborators** on a hub (face stack + invite sheet), read **§4.7** and **`.cursor/rules/exxat-collaboration-access.mdc`** / **`.cursor/skills/exxat-collaboration-access/SKILL.md`**.
23
+ 11. **Before** adding **onboarding tours, feature walkthroughs, or coach marks**, read **§11** and `references/coach-marks.md`.
24
+ 12. **Before** changing the **global command palette (⌘K)** or search/AI entry UX, read **§7.1** and **`docs/command-menu-pattern.md`**.
25
+ 13. **Before** choosing **drawer vs dialog vs new page** for a task flow, read **§6.4**, **`docs/data-views-pattern.md`** (Page vs drawer), and **`docs/drawer-vs-dialog-pattern.md`** (modal vs side panel on the same route).
26
+ 14. **Before** adding **success/error/confirmation feedback**, read **§6.5** and **`.cursor/rules/exxat-no-toast.mdc`** (no toast or snackbars).
27
+ 15. Prefer **composing existing components** over new one-off UI. If something is missing, **extend** shared components under `components/`, not a single page file.
27
28
  - **MUST** scan `components/` (especially `components/ui/`, `components/data-views/`, `components/templates/`, `components/key-metrics.tsx`, `components/page-header.tsx`, and the charts/banner/dot-pattern surfaces) **before** writing any new UI. If a primitive or composition already exists, **use it** — don't build a parallel one.
28
- - **Examples of existing surfaces to reuse:** card grid → `ListPageBoardCard` + `BoardCardIconRow` / `BoardCardTwoLineBlock`; AI / dot animation → `AiThinkingOverlay` + `DotPattern`; search input → `InputGroup` + `InputGroupAddon` + `InputGroupInput`; page title → `PageHeader` (serif via `font-heading`); list hub shell → `ListPageTemplate` (`metrics`, `defaultTabs`, `renderContent`); metrics strip → `KeyMetrics`; **view body gutter + centered max-width** → **`ListPageViewFrame`** (**§4.5**).
29
- 15. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
30
- 16. **Before** adding entity **mock data**, a **new view tab**, or **detail/inspector** panels on a list hub, read **`.cursor/rules/exxat-centralized-list-dataset.mdc`** and **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** (single **`useTableState`** row bag for every view; **no** parallel mock arrays per view).
29
+ - **Examples of existing surfaces to reuse:** card grid → `ListPageBoardCard` + `BoardCardIconRow` / `BoardCardTwoLineBlock`; AI / dot animation → `AiThinkingOverlay` + `DotPattern`; search input → `InputGroup` + `InputGroupAddon` + `InputGroupInput`; page title → `PageHeader` (serif via `font-heading`); list hub shell → `ListPageTemplate` (`metrics`, `defaultTabs`, `renderContent`); metrics strip → `KeyMetrics`; **view body gutter + centered max-width** → **`ListPageViewFrame`** (**§4.5**); **shared access / invite** → **`PageHeader` `variant="collaboration"`** + **`InviteCollaboratorsDrawer`** (**§4.7**).
30
+ - **If** nothing fits and you would add a **new shared primitive or large bespoke widget**: **ask the user** for direction first — **`.cursor/rules/exxat-reuse-before-custom.mdc`** (unless the task already explicitly approved a greenfield build).
31
+ 16. **Match** naming, imports, and patterns of the nearest reference implementation (usually Placements).
32
+ 17. **Before** adding entity **mock data**, a **new view tab**, or **detail/inspector** panels on a list hub, read **`.cursor/rules/exxat-centralized-list-dataset.mdc`** and **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`** (single **`useTableState`** row bag for every view; **no** parallel mock arrays per view).
33
+ 18. **Before** choosing **cards vs table rows vs simple lists** for a hub, read **`docs/card-vs-rows-pattern.md`** and **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
34
+ 19. **Before** adding **`KeyMetrics`** strips on list hubs or dashboard key-metrics cards, read **`docs/kpi-strip-max-four-pattern.md`** and **`.cursor/rules/exxat-kpi-max-four.mdc`** (at most **four** tiles).
35
+ 20. **Before** adding **new shared UI primitives** or bespoke widgets when nothing in **`components/`** fits after scanning, follow **`.cursor/rules/exxat-reuse-before-custom.mdc`** — **ask the user** for direction unless the task already approved a greenfield build.
31
36
 
32
- **Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/command-menu-pattern.md` (keep in sync with this handbook for big refactors).
37
+ **Longer narrative and architecture:** `docs/data-views-pattern.md`, `docs/drawer-vs-dialog-pattern.md`, `docs/card-vs-rows-pattern.md`, `docs/kpi-strip-max-four-pattern.md`, `docs/command-menu-pattern.md`, `docs/collaboration-access-pattern.md` (keep in sync with this handbook for big refactors).
33
38
 
34
39
  ---
35
40
 
@@ -37,8 +42,8 @@ Cross-cutting Cursor rules also live in the repo root `.cursor/rules/` (data tab
37
42
 
38
43
  1. **User / task instructions** in the current session (highest).
39
44
  2. This **`AGENTS.md`** for Exxat DS product patterns.
40
- 3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
41
- 4. Project **skills** under `.cursor/skills/` when relevant — e.g. **shadcn**, **exxat-accessibility** (WCAG / ARIA / touch / contrast), **exxat-board-cards** (kanban card shell, status badges, primitives), **exxat-list-page-view-shells** (centered view bodies, **`ListPageViewFrame`**), **exxat-centralized-list-dataset** (one **`useTableState`** row bag + shared maps across all list-hub views and **`TablePropertiesDrawer`**).
45
+ 3. **`.cursor/rules/*.mdc`** at repo root (`exxat-data-tables`, `exxat-list-page-connected-views`, `exxat-centralized-list-dataset`, `exxat-list-page-view-shells`, `exxat-table-properties-drawer`, `exxat-board-cards`, `exxat-page-vs-drawer`, `exxat-drawer-vs-dialog`, `exxat-card-vs-list-rows`, `exxat-kpi-max-four`, `exxat-reuse-before-custom`, `exxat-no-toast`, `exxat-kbd-shortcuts`, `exxat-accessibility`, `exxat-fontawesome-icons`, `exxat-primary-nav-secondary-panel`, `exxat-collaboration-access`, `exxat-ds-agents`) and any rules under **`exxat-ds/.cursor/rules/`** (including **`exxat-dashboard-view-charts`** for Data view charts).
46
+ 4. Project **skills** under `.cursor/skills/` when relevant — e.g. **shadcn**, **exxat-accessibility** (WCAG / ARIA / touch / contrast), **exxat-board-cards** (kanban card shell, status badges, primitives), **exxat-list-page-view-shells** (centered view bodies, **`ListPageViewFrame`**), **exxat-centralized-list-dataset** (one **`useTableState`** row bag + shared maps across all list-hub views and **`TablePropertiesDrawer`**), **exxat-collaboration-access** (face rail + invite sheet + library access), **exxat-dedicated-search-surfaces** (landing vs results split, **`DedicatedSearch*`** templates + recents without hydration drift), **exxat-drawer-vs-dialog**, **exxat-card-vs-list-rows**, **exxat-kpi-max-four**.
42
47
 
43
48
  If two documents conflict, prefer the **more specific** rule for the file type, then **newer** team decisions captured in `AGENTS.md`.
44
49
 
@@ -77,7 +82,7 @@ If two documents conflict, prefer the **more specific** rule for the file type,
77
82
 
78
83
  **MUST NOT** ship a **new primary nav hub** as an **empty or placeholder-only page** (e.g. a paragraph saying “replace this later” with no **`DataTable`**, mock data, or connected views). When a route is linked from **`lib/mock/navigation.tsx`**, land users on the same **hub stack** as Team / Placements: **`ListPageTemplate`** + typed mock rows (typically **≥ ~12**), search, filters, **`TablePropertiesDrawer`**, and all view tabs the template supports (**§4.1**), unless the product explicitly scopes a route as a non-data shell (rare).
79
84
 
80
- **Mock data:** Put typed row arrays in **`lib/mock/<entity>.ts`**. Add **`lib/mock/<entity>-kpi.ts`** (or colocated helpers) with pure functions **`entityKpiMetrics(rows)`** / **`entityKpiInsight(rows)`** returning **`MetricItem[]`** / **`MetricInsight`** for **`KeyMetrics`**. The page client passes full mock rows into one table component; KPI helpers receive **`tableState.rows`** inside that component so search/filters apply to list, board, dashboard, and table together.
85
+ **Mock data:** Put typed row arrays in **`lib/mock/<entity>.ts`**. Add **`lib/mock/<entity>-kpi.ts`** (or colocated helpers) with pure functions **`entityKpiMetrics(rows)`** / **`entityKpiInsight(rows)`** returning **`MetricItem[]`** / **`MetricInsight`** for **`KeyMetrics`**. Each **`MetricItem`** must set **`trend`** to match the signed change; use **`trendPolarity`** when an increase is **not** favorable (defects, review flags, overdue — see **`docs/kpi-trend-pattern.md`** and **`.cursor/rules/exxat-kpi-trends.mdc`**). **`entityKpiMetrics`** for **`ListPageTemplate`** metrics and Data-tab key-metrics cards: return **at most four** **`MetricItem`** — **`docs/kpi-strip-max-four-pattern.md`**, **`lib/dashboard-layout-merge.ts`** (`KEY_METRICS_KPI_COUNT_MAX`), **`.cursor/rules/exxat-kpi-max-four.mdc`**. The page client passes full mock rows into one table component; KPI helpers receive **`tableState.rows`** inside that component so search/filters apply to list, board, dashboard, and table together.
81
86
 
82
87
  **Centralized dataset (rows + table properties + alternate views):** **MUST** use one **`useTableState`** row bag for the **`DataTable`**, **`TablePropertiesDrawer`** (columns/density on **that** table), and **every** record-bearing **`DataListViewType`** — **folder**, **panel**, **tree**, etc. — via **`tableState.rows`**. **MUST NOT** import a second **`lib/mock/<entity>`** array into a view-only module while the grid filters state; **MUST NOT** fork a duplicate row type for inspectors. Shared **properties**: tab labels **`DATA_LIST_VIEW_TILES`** (`lib/data-list-view.ts`), status **`lib/list-status-badges.ts`**, KPI helpers from **`tableState.rows`**. **Presentation:** non-table bodies use **`ListPageViewFrame`** and **`components/data-views/`** primitives fed by the **same** **`tableState.rows`** (**§4.5**). **Rule + skill:** **`.cursor/rules/exxat-centralized-list-dataset.mdc`**, **`.cursor/skills/exxat-centralized-list-dataset/SKILL.md`**.
83
88
 
@@ -159,6 +164,40 @@ Thread **`view`** and **`onViewChange`** from the **client** → **table / toolb
159
164
 
160
165
  **Cursor rule:** **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**. **Icons in panel:** **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
161
166
 
167
+ ### 4.7 Collaboration & access (shared hubs)
168
+
169
+ **Use when** a hub is **shared** and users need a **who has access** roster plus **invite by email** with **library access** (Owner / Editor / Commenter / Viewer). **Directory role tags** (Faculty, Program coordinator, Director) are **separate** from library access.
170
+
171
+ **MUST:**
172
+
173
+ | Step | Action |
174
+ |------|--------|
175
+ | **Header** | **`PageHeader`** **`variant="collaboration"`** with **`collaborators`**; **empty roster** → outline **Add collaborator**; **non-empty** → face rail (faces / **`+N`** open the invite sheet). |
176
+ | **Invite entry** | **⋯ More** → **Invite people**; header empty CTA / face rail → **`InviteCollaboratorsDrawer`** (floating **`Sheet`**, same family as **`ExportDrawer`**). |
177
+ | **Hub client** | Prefer **`CollaborationAccessFlow`** (or own **`collaborators`** + **`inviteOpen`**); successful invite updates **`collaborators`** for header + sheet. |
178
+ | **Types** | **`PageHeaderCollaborator`** + **`lib/collaborator-access.ts`** — **one** access map per product; customize invite copy per hub, not enum forks. |
179
+ | **Roster** | Single bordered list, row dividers; **name → email → role tags**; trailing **access** badge. |
180
+ | **Invite field** | **`FieldGroup`** + **`Field`**; email + access in **`InputGroup`** (**`InputGroupInput`** + **`InputGroupAddon`** **`Select`** with **`SelectGroup`**); **`FieldDescription`** for email format; **no** toast (**§6.5**). |
181
+
182
+ **MUST NOT:** **`Select`** in **`InputGroupAddon`** without **`InputGroupInput`** / **`SelectGroup`**; per-person cards in the roster; a second invite control **beside** an existing face rail.
183
+
184
+ **Narrative:** **`docs/collaboration-access-pattern.md`**. **Cursor rule:** **`.cursor/rules/exxat-collaboration-access.mdc`**. **Skill:** **`.cursor/skills/exxat-collaboration-access/SKILL.md`**. **Reference:** Question bank header + client + **`InviteCollaboratorsDrawer`**.
185
+
186
+ ### 4.8 Dedicated search (landing vs results)
187
+
188
+ **Use when** a hub uses **one primary query param** (typically **`?q=`**) with two product states: **empty** → centered **landing** (composer ± recents) vs **non-empty** → **`ListPageTemplate`** / **`DataTable`** results on the same hub stack.
189
+
190
+ **MUST:**
191
+
192
+ | Step | Action |
193
+ |------|--------|
194
+ | **Templates** | **`DedicatedSearchLandingTemplate`** for the empty-query shell; **`DedicatedSearchResultsHeaderChrome`** + **`DEDICATED_SEARCH_RESULTS_OUTER_CONTENT_CLASSNAME`** for the results branch chrome. |
195
+ | **Composer** | **`DedicatedSearchUrlComposer`** — hub passes **`patchSearchParams`** (preserve scope / feature flags while merging text) and optional **`onRecordSubmission`**. |
196
+ | **Recents** | **`DedicatedSearchRecents`** + **`createDedicatedSearchRecentsController`** — **MUST NOT** read **`localStorage`** in **`useState`** initializers (**hydration**). |
197
+ | **Naming** | Keep **`DedicatedSearch*`** / **`dedicated-search-*`** generic; domain copy + patchers live next to the hub (**`lib/<entity>-dedicated-search.ts`**) or inline in the hub client. |
198
+
199
+ **Cursor rule:** **`.cursor/rules/exxat-dedicated-search-surfaces.mdc`**. **Skill:** **`.cursor/skills/exxat-dedicated-search-surfaces/SKILL.md`**.
200
+
162
201
  ---
163
202
 
164
203
  ## 5. Layout alignment (avoid double inset)
@@ -194,18 +233,18 @@ Match **Placements**:
194
233
 
195
234
  **MUST NOT** treat a main hub table page as a “light” sub-section: use the same shell as Placements (tabs, optional metrics strip, template-level export).
196
235
 
197
- ### 6.4 Page vs drawer (actions and auxiliary views)
236
+ ### 6.4 Page vs drawer vs dialog (actions and auxiliary views)
198
237
 
199
- **SHOULD** choose the surface by whether the user must keep **page context** while acting:
238
+ **SHOULD** choose the surface by whether the user must keep **page context** while acting, and whether the hub may stay **interactable**:
200
239
 
201
- | Use a **drawer / sheet** (side panel) | Use a **new page** (dedicated route) |
202
- |--------------------------------------|----------------------------------------|
203
- | The user needs **the current page behind them** (list, hub, or parent task) **and** a **quick view**, **quick actions**, or a **short auxiliary step** | The flow is **primary**, **long-form**, **multi-step**, or should have its **own URL**, bookmark, or history entry **without** the parent page visible |
204
- | Examples: table/column properties, export, glance at row metadata, lightweight “do one thing and return” | Examples: full create/edit forms, wizards, deep detail that is the main task |
240
+ | Use a **drawer / sheet** (side panel) | Use a **dialog** (modal) | Use a **new page** (dedicated route) |
241
+ |--------------------------------------|---------------------------|----------------------------------------|
242
+ | The user needs **the current page behind them** (list, hub, or parent task) **and** a **quick view**, **quick actions**, or a **short auxiliary step** — e.g. properties, export, invite | A **blocking** short choice — confirm/alert/destructive ack — where the page **must not** stay interactable until answered | The flow is **primary**, **long-form**, **multi-step**, or should have its **own URL**, bookmark, or history entry **without** the parent page visible |
243
+ | Examples: table/column properties, export, glance at row metadata | Examples: `AlertDialog`, delete confirm, compact “save changes?” | Examples: full create/edit forms, wizards, deep detail that is the main task |
205
244
 
206
- **Rationale:** Drawers preserve **spatial context** and reduce navigation churn; full pages avoid cramming complex work into a narrow overlay.
245
+ **Rationale:** Drawers preserve **spatial context**; dialogs enforce **focus**; full pages avoid cramming complex work into overlays.
207
246
 
208
- **Details:** `docs/data-views-pattern.md` (Page vs drawer). Root **`.cursor/rules/exxat-page-vs-drawer.mdc`**.
247
+ **Details:** `docs/data-views-pattern.md` (Page vs drawer), **`docs/drawer-vs-dialog-pattern.md`** (drawer vs modal on the same route). Root **`.cursor/rules/exxat-page-vs-drawer.mdc`**, **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**.
209
248
 
210
249
  ### 6.5 Messaging — no toast
211
250
 
@@ -391,6 +430,8 @@ Reference: `components/new-placement-form.tsx` (Next/Back buttons); full shortcu
391
430
  | Full dashboard route | `DashboardTabs`, `KeyMetrics`, `ChartsOverview` | `app/(app)/dashboard/page.tsx`, `components/dashboard-tabs.tsx` |
392
431
  | Board cards | **`ListPageBoardCard`** + primitives + entity card (**§4.4**) | `components/data-views/list-page-board-card.tsx`, `board-card-primitives.tsx`, `placement-board-card.tsx` |
393
432
  | **Application sidebar** (school/program, product, profile, child nav) | **`AppSidebar`**, **`TeamSwitcher`**, **`NavUser`**, collapsible + **popover** (icon rail) | `components/app-sidebar.tsx`, `nav-user.tsx`, `product-switcher.tsx`, `lib/mock/navigation.tsx`, `lib/logo-dev.ts`, `lib/stock-portrait.ts` — patterns in **exxat-ds-skill §3.1** |
433
+ | **Collaboration & access** (face rail + invite sheet) | **`PageHeader` `variant="collaboration"`**, **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`** | `components/page-header.tsx`, `components/invite-collaborators-drawer.tsx`, `components/question-bank-page-header.tsx`, `components/question-bank-client.tsx`, **`§4.7`**, **`docs/collaboration-access-pattern.md`** |
434
+ | **Dedicated search** (empty `?q=` landing vs results) | **`DedicatedSearchLandingTemplate`**, **`DedicatedSearchUrlComposer`**, **`DedicatedSearchRecents`**, **`DedicatedSearchResultsHeaderChrome`**, **`lib/dedicated-search-recents.ts`** | **`§4.8`**, **`components/templates/dedicated-search-*`**, **`components/dedicated-search-*.tsx`** |
394
435
  | Persistence (example) | Page + lifecycle keys | `lib/data-list-persistence.ts`, `DataListClient` / `DataListTable` |
395
436
  | Coach marks / tours | `CoachMark`, `useCoachMark`, coach mark registry | `components/ui/coach-mark.tsx`, `hooks/use-coach-mark.ts`, `lib/coach-mark-registry.ts` |
396
437
  | Settings page | Coach mark management | `app/(app)/settings/page.tsx`, `components/settings-client.tsx` |
@@ -403,7 +444,7 @@ Reference: `components/new-placement-form.tsx` (Next/Back buttons); full shortcu
403
444
 
404
445
  - **Product (Exxat One / Prism):** Use **`ExxatProductLogo`** for the header product control and **`ProductSwitcher`** — do **not** substitute logo.dev rasters unless product explicitly requests it.
405
446
  - **School logos:** Use **`logoDevUrl()`** from **`lib/logo-dev.ts`** in **`NAV_SCHOOLS`**; optional env **`NEXT_PUBLIC_LOGO_DEV_TOKEN`**.
406
- - **Team / program dropdown:** Override **`DropdownMenuContent`** default **`w-(--radix-dropdown-menu-trigger-width)`** for the school switcher (e.g. **`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) so long names are not forced to wrap like the narrow sidebar trigger. **Do not truncate** school or program labels; wrap with **`items-start`**, **`break-words`**, **`whitespace-normal`**. Selected-school summary shows **school + current program**.
447
+ - **Team / program dropdown:** The shared **`DropdownMenuContent`** uses **intrinsic width** (**`min-w-52 w-max`** + viewport-capped **`max-w`**) so view menus and table actions are not squeezed to the trigger. The **school / program** switcher still passes an explicit wider surface (**`!w-max min-w-72 max-w-[min(100vw-2rem,28rem)]`**) for long labels. **Do not truncate** school or program labels; wrap with **`items-start`**, **`break-words`**, **`whitespace-normal`**. Selected-school summary shows **school + current program**.
407
448
  - **Team switcher trigger:** **`SidebarMenuButton` `size="lg"`** is **`h-12`** + **`overflow-hidden`** and **clips** the program line — when expanded or mobile, use **`h-auto min-h-12`** and **`overflow-x-clip overflow-y-visible`**; on **icon rail**, hide text with **`group-data-[collapsible=icon]:hidden`**.
408
449
  - **Nav items with children:** **Popover** on desktop **icon rail**; **Collapsible** when expanded. **MUST NOT** use **`SidebarMenuButton` `tooltip={…}`** as the **direct** child of **`CollapsibleTrigger asChild`** (extra **`Tooltip` root** breaks Radix **`Slot`** / **`React.Children.only`**).
409
450
  - **Mock profile photo:** **`stockPortraitUrl()`** from **`lib/stock-portrait.ts`**; **`AvatarImage`** **`referrerPolicy="no-referrer"`** for external URLs.
@@ -463,6 +504,10 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
463
504
  ## 12. Documentation
464
505
 
465
506
  - **Deep dive:** `docs/data-views-pattern.md` (includes **Page vs drawer** with **§6.4**)
507
+ - **Drawer vs dialog (same route):** `docs/drawer-vs-dialog-pattern.md` — **`.cursor/rules/exxat-drawer-vs-dialog.mdc`**
508
+ - **Cards vs table rows:** `docs/card-vs-rows-pattern.md` — **`.cursor/rules/exxat-card-vs-list-rows.mdc`**
509
+ - **KPI strip (max four tiles):** `docs/kpi-strip-max-four-pattern.md` — **`.cursor/rules/exxat-kpi-max-four.mdc`**
510
+ - **KPI deltas & trend arrows:** `docs/kpi-trend-pattern.md` (`MetricItem.trendPolarity`, `KeyMetrics`, chart mini-metrics)
466
511
  - **Global command palette (⌘K):** `docs/command-menu-pattern.md`
467
512
  - **No toast / snackbars:** **§6.5**, root **`.cursor/rules/exxat-no-toast.mdc`**
468
513
  - **This handbook:** `exxat-ds/AGENTS.md` (keep checklist sections updated when patterns change)
@@ -479,7 +524,10 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
479
524
  | Match Placements for export + primary CTA + More menu | Outline button as the single primary CTA on exportable pages |
480
525
  | Pair `Kbd` hints with real shortcuts | Browser-reserved chords for app actions |
481
526
  | Global palette: **§7.1** — search + quick in-menu AI vs **Ask Leo**; **`dataGroups`** + **`searchOnly`** for bulky indexes | Palette as link-only dump; AI that belongs in **Ask Leo** forced into the palette; mounting full **`dataGroups`** on open when **`searchOnly`** should hide them |
482
- | **§6.4** — drawer when **page context + quick** view/actions; **new page** for primary / long / own-URL flows | Forcing **full workflows** into a drawer when a route fits; or **routing** for tasks that are only quick glances over a hub |
527
+ | **§6.4** — **drawer** when **page context + quick** view/actions; **dialog** for **blocking** confirm/alert/short choice; **new page** for primary / long / own-URL flows | Forcing **full workflows** into a drawer when a route fits; using a **dialog** when users must **reference** the grid (prefer drawer); **routing** for tasks that are only quick glances over a hub |
528
+ | **KPI strips** — **≤ 4** `MetricItem` per **`KeyMetrics`** on template metrics + Data-tab key-metrics cards (**`KEY_METRICS_KPI_COUNT_MAX`**) | Fifth+ headline tile in the same strip; duplicate tiles to pad count |
529
+ | **Cards vs rows** — **DataTable** for dense comparable hubs; **`ListPageBoardCard`** / **`ListPageViewFrame`** when visual/kanban/folder — **`docs/card-vs-rows-pattern.md`** | Card walls for **50+** homogeneous records where the product expects **sort/filter/compare** without a deliberate UX exception |
530
+ | **Reuse before custom** — scan **`components/`** + **§9**; **ask the user** before new shared primitives or large bespoke widgets — **`exxat-reuse-before-custom.mdc`** | Parallel stacks; silent new “table” or metric systems when **`DataTable`** / **`KeyMetrics`** already apply |
483
531
  | **§6.5** — feedback via **banners / inline / dialogs** — **no** toast or snackbar | **`toast()`** / **Sonner** / transient corner notifications for product messaging |
484
532
  | Meet **§8** + **`exxat-accessibility`** skill (ARIA, 24px targets, contrast, **§8.3** min **11px** text, overlay titles) | `tablist` mixing non-tabs; **16px** sole targets; dialogs without titles; text below **11px** (except legally required fine print) |
485
533
  | Use `CoachMark` + `useCoachMark` for onboarding tours (§11); register in `coach-mark-registry` | Build one-off walkthrough overlays or custom onboarding modals |
@@ -487,6 +535,8 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
487
535
  | Board cards: **`ListPageBoardCard`** shell; status via **`ListHubStatusBadge`** + **`lib/list-status-badges`**; no **`uppercase`** on status chips (§4.4) | One-off board card markup; status as plain body text; duplicated status maps outside **`list-status-badges`**; **empty placeholder** primary hubs (§4.1) |
488
536
  | **§4.5** — Non-table view bodies use **`ListPageViewFrame`** (+ **`data-views/`** shells); new grids are generic components, not route-only markup | Duplicated `mx-4` / `max-w-*` per hub; wrapping **`DataTable`** so inset **doubles** (**§5**) |
489
537
  | **§4.6** — **`secondaryPanel`** + **`PANELS`** + **`useAutoPanel`** together for nested scope nav | **`secondaryPanel`** id with no panel component or activator |
538
+ | **§4.7** — **`PageHeader` `variant="collaboration"`** + **`CollaborationAccessFlow`** / **`InviteCollaboratorsDrawer`**; empty **Add collaborator** + non-empty face rail; roster + invite from **`collaborator-access.ts`** | Extra invite beside a populated face rail; per-person roster cards; forked access enums; toast on invite |
539
+ | **§4.8** — **`DedicatedSearch*`** templates + composer + recents; **no** `localStorage` in **`useState`** initial paint; hub-specific **`patchSearchParams`** only | Forked `*QuestionBank*SearchLanding*` shells for another entity; hydration mismatch on recents |
490
540
  | **Font Awesome** — Kit in **`app/layout.tsx`**; **`fa-light` / `fa-solid`** conventions; **`aria-hidden`** on decorative **`<i>`**; run **`fa:subset-audit`** when adding glyphs (**`exxat-fontawesome-icons.mdc`**) | Parallel icon libraries for the same product chrome |
491
541
 
492
542
  ---
@@ -496,7 +546,7 @@ New pages **SHOULD** namespace keys and version JSON (`v: 1`) for future migrati
496
546
  Copy and complete when implementing or reviewing:
497
547
 
498
548
  - [ ] **Centralized dataset:** One **`useTableState`** / **`tableState.rows`** for **all** view tabs and inspectors; **TablePropertiesDrawer** on the **same** `DataTable`; **no** parallel mock arrays per view — **`.cursor/rules/exxat-centralized-list-dataset.mdc`**.
499
- - [ ] **Reuse:** `ListPageTemplate`, `DataTable` / `useTableState`, `TablePropertiesDrawer` — no parallel bespoke tabs/filters.
549
+ - [ ] **Reuse:** `ListPageTemplate`, `DataTable` / `useTableState`, `TablePropertiesDrawer` — no parallel bespoke tabs/filters. **New shared primitives:** **ask the user** after scanning **`components/`** + **§9** — **`.cursor/rules/exxat-reuse-before-custom.mdc`**.
500
550
  - [ ] **Tabs:** Any main `DataTable` sits under `ListPageTemplate` with appropriate view tabs.
501
551
  - [ ] **Inset:** No double horizontal padding around `DataTable`.
502
552
  - [ ] **§4.5 View shells:** Folder / panel / icon views use **`ListPageViewFrame`** (or a **`data-views/`** component that uses it); no page-tied-only grid wrappers; **`DataTable`** not double-wrapped (**§5**).
@@ -508,7 +558,9 @@ Copy and complete when implementing or reviewing:
508
558
  - [ ] **Data view dashboard (Placements / Team / Compliance):** Charts use **`ChartFigure`** + **`ChartDataTable`**; **Edit layout** on toolbar; **`activeBar` / `activeShape`** keyboard styling from **`lib/chart-keyboard-selection`** — not opacity-only **`Cell`** hacks (§4.3).
509
559
  - [ ] **Dashboard layout persistence:** **`lib/data-view-dashboard-storage`** (or **`saveDashboardLayout`** / **`loadDashboardLayout`** on Placements); **`mergeDashboardLayout`** on load — no new ad-hoc storage keys for the same layout (§4.3).
510
560
  - [ ] **⌘K palette (§7.1):** If adding or changing **`dataGroups`**, map rows in **`lib/command-menu-search-data.ts`** (not `command-menu.tsx`); use **`searchOnly`** on bulky groups; keep **`docs/command-menu-pattern.md`** aligned.
511
- - [ ] **Page vs drawer (§6.4):** Quick auxiliary actions with **parent context** → drawer/sheet; primary or long flows → **new route** — see **`docs/data-views-pattern.md`**.
561
+ - [ ] **Page vs drawer vs dialog (§6.4):** Quick auxiliary with **parent context** and interactable hub **drawer/sheet**; **blocking** short confirm → **dialog**; primary or long flows → **new route** — **`docs/data-views-pattern.md`**, **`docs/drawer-vs-dialog-pattern.md`**.
562
+ - [ ] **Cards vs rows:** Primary sortable hub with many homogeneous records → **`DataTable`**; kanban / visual tiles → **`ListPageBoardCard`** — **`docs/card-vs-rows-pattern.md`**, **`.cursor/rules/exxat-card-vs-list-rows.mdc`**.
563
+ - [ ] **KPI count (max four):** **`entityKpiMetrics`** (and any static **`MetricItem[]`** for the same strip) has **≤ 4** tiles for template metrics + Data-tab key-metrics — **`docs/kpi-strip-max-four-pattern.md`**, **`.cursor/rules/exxat-kpi-max-four.mdc`**.
512
564
  - [ ] **No toast (§6.5):** No **`toast()`** / Sonner / snackbars — use banners, inline status, or dialogs.
513
565
  - [ ] **Typography (§8.3):** No visible copy below **11px** — use **`text-xs`** (`--text-xs` in **`globals.css`**); board/list cards use **`text-xs`** / **`text-sm`** for body lines.
514
566
  - [ ] **Board cards (§4.4):** **`ListPageBoardCard`** + hierarchy (title → badge row → body); **`ListPageBoardCardAvatar`** when appropriate; status via **`ListHubStatusBadge`** + **`lib/list-status-badges`** — **not** `uppercase` on labels; **`BoardCardTwoLineBlock`** for stacked facts.
@@ -517,10 +569,13 @@ Copy and complete when implementing or reviewing:
517
569
  - [ ] **Kbd:** Follow `exxat-kbd-shortcuts.mdc` if adding shortcuts or hints.
518
570
  - [ ] **Accessibility:** §8 — tablist/toolbar patterns, **≥24px** targets for icon-only controls, contrast on tinted surfaces, dialog/sheet/drawer **titles**; **every icon that communicates info has a text alternative** — adjacent label (preferred) OR `aria-label` + `Tooltip` (§8.6 Case A/B/C, covers informational icons like calendar-for-date, status dots, AND icon-only buttons); **kbd inside a button uses `<Kbd variant="bare">`** (§8.7); re-run **axe** on Placements when changing views toolbar.
519
571
  - [ ] **Coach marks (§11):** `CoachMark` + `useCoachMark`; register in **`coach-mark-registry`**; use **`enabled`** / **`dependsOnDismissedFlowId`** when a tour must wait for another flow or a specific view (e.g. **dashboard**); customize-dashboard flows use **`lib/dashboard-customize-coach-mark.ts`**.
520
- - [ ] **Application sidebar (§9.1):** **`ExxatProductLogo`** for product; **`logoDevUrl`** for schools; team switcher **`DropdownMenuContent`** not trigger-width-only (**`!w-max`** + min/max width); expanded switcher **`h-auto min-h-12`**; no **`CollapsibleTrigger` → `SidebarMenuButton` with `tooltip` prop**; child links **popover** on icon rail; profile **`stockPortraitUrl`** + **`referrerPolicy="no-referrer"`** on **`AvatarImage`**.
572
+ - [ ] **Application sidebar (§9.1):** **`ExxatProductLogo`** for product; **`logoDevUrl`** for schools; team switcher **`DropdownMenuContent`** keeps the explicit wide school/program surface (**`!w-max`** + min/max width); expanded switcher **`h-auto min-h-12`**; no **`CollapsibleTrigger` → `SidebarMenuButton` with `tooltip` prop**; child links **popover** on icon rail; profile **`stockPortraitUrl`** + **`referrerPolicy="no-referrer"`** on **`AvatarImage`**.
521
573
  - [ ] **Secondary panel (§4.6):** If **`NavLinkItem.secondaryPanel`** is set — **`PANELS[id]`** in **`secondary-panel.tsx`**, hub mounts **`useAutoPanel(id)`**, scope syncs to URL + **`tableState.rows`** — **`.cursor/rules/exxat-primary-nav-secondary-panel.mdc`**.
574
+ - [ ] **Collaboration & access (§4.7):** Shared hubs use **`variant="collaboration"`**, empty **Add collaborator** / non-empty face rail, **⋯ → Invite people**, **`CollaborationAccessFlow`** or **`InviteCollaboratorsDrawer`**, **`lib/collaborator-access.ts`**, roster **name → email → role tags** — **`.cursor/rules/exxat-collaboration-access.mdc`**.
575
+ - [ ] **Dedicated search (§4.8):** Landing uses **`DedicatedSearchLandingTemplate`**; results use **`DedicatedSearchResultsHeaderChrome`** + outer **`DEDICATED_SEARCH_RESULTS_OUTER_CONTENT_CLASSNAME`**; **`DedicatedSearchUrlComposer`** + **`DedicatedSearchRecents`** with **`createDedicatedSearchRecentsController`** — **`.cursor/rules/exxat-dedicated-search-surfaces.mdc`**.
576
+ - [ ] **KPI trends:** **`MetricItem.trend`** matches the delta direction; **`trendPolarity`** set for “more is worse” metrics (flags, defects, overdue) — **`docs/kpi-trend-pattern.md`**, **`.cursor/rules/exxat-kpi-trends.mdc`**.
522
577
  - [ ] **Font Awesome:** New glyphs covered by **`fa:subset-audit`** / Kit subset; decorative **`<i>`** has **`aria-hidden`**; icon-only controls follow **§8.6** — **`.cursor/rules/exxat-fontawesome-icons.mdc`**.
523
578
 
524
579
  ---
525
580
 
526
- *Last updated: §4.1 centralized dataset + presentation; §4.5–§4.6 view shells + secondary panel; Font Awesome rule; §9.1 application sidebar shell; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
581
+ *Last updated: drawer vs dialog / card vs rows / KPI max-four pattern docs + rules + skills; §6.4 table; §4.8 dedicated search templates; §4.7 collaboration & access; §4.1 centralized dataset + presentation; §4.5–§4.6 view shells + secondary panel; Font Awesome rule; §9.1 application sidebar shell; §4.4 board cards; §6.5 no toast; §7.1 command palette; §13 checklist.*
@@ -5,7 +5,8 @@ import { Button } from "@/components/ui/button"
5
5
  const LINKS = [
6
6
  { href: "/dashboard", label: "Dashboard", description: "Metrics, charts, and layout patterns." },
7
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." },
8
+ { href: "/question-bank", label: "Question bank", description: "Discovery hub for browsing folders, recents, and AI-assisted create/import flows." },
9
+ { href: "/question-bank/library", label: "Question library", description: "Folders, OS folder view, panel, and tree demos on mock items." },
9
10
  { href: "/settings", label: "Settings", description: "Appearance, tours, and shell preferences." },
10
11
  { href: "/help", label: "Help", description: "Support and documentation entry points." },
11
12
  ] as const
@@ -28,6 +28,12 @@ export default function HelpPage() {
28
28
  <Link href="/settings#appearance">App settings</Link>
29
29
  </Button>
30
30
  </div>
31
+ <section id="more" className="scroll-mt-20 mt-10">
32
+ <h2 className="text-sm font-semibold text-foreground">More</h2>
33
+ <p className="mt-1 text-sm text-muted-foreground leading-relaxed">
34
+ Additional resources and shortcuts from the sidebar land here when you choose More.
35
+ </p>
36
+ </section>
31
37
  </div>
32
38
  </PrimaryPageTemplate>
33
39
  )