@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.
- package/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
- package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
- package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
- package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
- package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
- package/consumer-extras/patterns/data-views-pattern.md +12 -4
- package/package.json +4 -1
- package/src/components/ui/banner.tsx +20 -7
- package/src/components/ui/date-picker-field.tsx +3 -3
- package/src/components/ui/dropdown-menu.tsx +17 -6
- package/src/components/ui/input-group.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/separator.tsx +2 -2
- package/src/components/ui/sidebar.tsx +31 -3
- package/src/components/ui/textarea.tsx +1 -1
- package/src/globals.css +0 -1
- package/src/index.ts +1 -0
- package/src/lib/date-filter.ts +13 -4
- package/src/lib/dropdown-menu-surface.ts +13 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
- package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
- package/template/.nvmrc +1 -1
- package/template/AGENTS.md +82 -27
- package/template/app/(app)/examples/page.tsx +2 -1
- package/template/app/(app)/help/page.tsx +6 -0
- package/template/app/(app)/layout.tsx +7 -4
- package/template/app/(app)/question-bank/find/page.tsx +12 -0
- package/template/app/(app)/question-bank/layout.tsx +46 -0
- package/template/app/(app)/question-bank/library/page.tsx +11 -0
- package/template/app/(app)/question-bank/list/page.tsx +12 -0
- package/template/app/(app)/question-bank/page.tsx +4 -3
- package/template/app/globals.css +1 -2
- package/template/components/app-sidebar.tsx +51 -13
- package/template/components/ask-leo-composer.tsx +173 -45
- package/template/components/ask-leo-sidebar.tsx +9 -1
- package/template/components/chart-area-interactive.tsx +3 -13
- package/template/components/charts-overview.tsx +33 -6
- package/template/components/collaboration-access-flow.tsx +144 -0
- package/template/components/compliance-page-header.tsx +1 -1
- package/template/components/compliance-table.tsx +2 -2
- package/template/components/dashboard-tabs.tsx +4 -3
- package/template/components/data-list-table-cells.tsx +1 -1
- package/template/components/data-list-table.tsx +1 -1
- package/template/components/data-table/index.tsx +5 -5
- package/template/components/data-table/use-table-state.ts +18 -2
- package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
- package/template/components/data-view-dashboard-charts-team.tsx +8 -5
- package/template/components/data-view-dashboard-charts.tsx +62 -227
- package/template/components/dedicated-search-recents.tsx +96 -0
- package/template/components/dedicated-search-url-composer.tsx +112 -0
- package/template/components/getting-started.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +10 -26
- package/template/components/invite-collaborators-drawer.tsx +453 -0
- package/template/components/key-metrics.tsx +54 -8
- package/template/components/nav-documents.tsx +1 -1
- package/template/components/new-placement-form.tsx +3 -3
- package/template/components/page-header.tsx +76 -59
- package/template/components/placements-board-view.tsx +3 -3
- package/template/components/placements-page-header.tsx +1 -1
- package/template/components/placements-table-columns.tsx +3 -2
- package/template/components/product-switcher.tsx +0 -1
- package/template/components/question-bank-board-view.tsx +35 -47
- package/template/components/question-bank-client.tsx +293 -81
- package/template/components/question-bank-dashboard-charts.tsx +174 -0
- package/template/components/question-bank-favorite-button.tsx +46 -0
- package/template/components/question-bank-hub-client.tsx +436 -0
- package/template/components/question-bank-list-view.tsx +26 -19
- package/template/components/question-bank-new-folder-sheet.tsx +56 -42
- package/template/components/question-bank-os-folder-view.tsx +3 -14
- package/template/components/question-bank-page-header.tsx +85 -53
- package/template/components/question-bank-panel-activator.tsx +3 -4
- package/template/components/question-bank-secondary-nav.tsx +523 -65
- package/template/components/question-bank-table.tsx +125 -343
- package/template/components/secondary-panel.tsx +130 -63
- package/template/components/settings-client.tsx +3 -1
- package/template/components/sidebar-shell.tsx +2 -0
- package/template/components/sites-all-client.tsx +1 -1
- package/template/components/sites-table.tsx +1 -1
- package/template/components/system-banner-slot.tsx +2 -1
- package/template/components/table-properties/drawer.tsx +3 -3
- package/template/components/table-properties/sort-card.tsx +1 -1
- package/template/components/team-page-header.tsx +1 -1
- package/template/components/team-table.tsx +8 -4
- package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
- package/template/components/templates/dedicated-search-results-template.tsx +19 -0
- package/template/components/templates/discovery-hub-template.tsx +273 -0
- package/template/components/templates/list-page.tsx +11 -4
- package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
- package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
- package/template/docs/card-vs-rows-pattern.md +36 -0
- package/template/docs/collaboration-access-pattern.md +114 -0
- package/template/docs/data-views-pattern.md +12 -4
- package/template/docs/drawer-vs-dialog-pattern.md +50 -0
- package/template/docs/kpi-strip-max-four-pattern.md +29 -0
- package/template/docs/kpi-trend-pattern.md +43 -0
- package/template/fontawesome-subset.manifest.json +2 -2
- package/template/hooks/use-location-hash.ts +14 -8
- package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
- package/template/lib/ask-leo-route-context.ts +24 -0
- package/template/lib/collaborator-access.ts +92 -0
- package/template/lib/command-menu-config.ts +8 -1
- package/template/lib/command-menu-search-data.ts +11 -8
- package/template/lib/data-list-display-options.ts +1 -1
- package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
- package/template/lib/date-filter.ts +1 -0
- package/template/lib/dedicated-search-recents.ts +76 -0
- package/template/lib/dedicated-search-url.ts +23 -0
- package/template/lib/discovery-hub.ts +15 -0
- package/template/lib/list-status-badges.ts +1 -21
- package/template/lib/mock/navigation.tsx +4 -2
- package/template/lib/mock/placements.ts +9 -9
- package/template/lib/mock/question-bank-folders.ts +7 -0
- package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
- package/template/lib/mock/question-bank-inspector.ts +1 -2
- package/template/lib/mock/question-bank-kpi.ts +38 -26
- package/template/lib/mock/question-bank.ts +43 -16
- package/template/lib/question-bank-dedicated-search.ts +19 -0
- package/template/lib/question-bank-hub-search.ts +90 -0
- package/template/lib/question-bank-nav.ts +322 -6
- package/template/lib/question-bank-recent-searches.ts +22 -0
- 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:
|
|
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(
|
|
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:
|
|
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
package/src/index.ts
CHANGED
package/src/lib/date-filter.ts
CHANGED
|
@@ -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
|
|
57
|
+
/** Format YYYY-MM-DD for filter chip labels (MM/DD/YYYY). */
|
|
49
58
|
export function formatYmdForDisplay(ymd: string): string {
|
|
50
|
-
const d =
|
|
51
|
-
if (
|
|
52
|
-
return d
|
|
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`**
|
|
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"
|
|
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
|
|
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
|
|
700
|
-
- [ ] **`Kbd` inside a `Button` uses `variant="bare"
|
|
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
|
-
|
|
1
|
+
22
|
package/template/AGENTS.md
CHANGED
|
@@ -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**
|
|
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 **
|
|
23
|
-
11. **Before**
|
|
24
|
-
12. **Before**
|
|
25
|
-
13. **Before**
|
|
26
|
-
14.
|
|
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
|
-
|
|
30
|
-
16. **
|
|
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
|
|
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
|
|
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:**
|
|
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;
|
|
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
|
|
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`**
|
|
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: "
|
|
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
|
)
|