// ───────────────────────────────────────────────────────────────────────────── // Mock data — Navigation // Uses .tsx because icons are JSX ( elements). // ───────────────────────────────────────────────────────────────────────────── import type { NavLinkItem, NavPrimaryLayout, NavSecondaryItem, } from "@exxatdesignux/ui/components/shell" import { flattenNavLayoutSpec, navLayoutToJsx, navToJsx, type NavLayoutSpec, type NavLinkSpec, } from "@exxatdesignux/product-framework" import { buildDesignOsNavLayout } from "@/lib/design-system/design-os-nav" import { logoDevUrl } from "@/lib/logo-dev" import { buildOneSchoolsNavLayout, ONE_SCHOOLS_HUB_SEGMENTS, } from "@/lib/one-schools-nav" import { stockPortraitUrl } from "@/lib/stock-portrait" import { productSlug, type Product } from "@/stores/app-store" import { customProductSlugFromSuffix } from "@/lib/product-routing" export { ONE_SCHOOLS_HUB_SEGMENTS } // ── Types ───────────────────────────────────────────────────────────────────── /** * Drill-in slot for sidebar rows that lead into deep, focused sections * (e.g. Settings → Profile / Organization / Billing). When a row carries * `drillIn` and the URL matches `sectionRouteRoot`, the primary sidebar * content area swaps to a stacked view rendered by `SidebarDrillIn` — * `[← Back] · ` followed by the section's nav list. * * Mutually exclusive with `secondaryPanel` — a row opts into EITHER the * drill-in pattern (deep section, user "going into" it) OR the secondary- * panel pattern (hub-scoped catalog, user "scoping" it). See * `apps/web/components/sidebar/sidebar-drill-in.tsx` and the * SidebarDrillIn primitive doc for the decision criteria. */ /** * The shell renders these, so `@exxatdesignux/ui` owns them and this module * re-exports them for the nav data below. They are the *resolved* form — icons * are React nodes, a drill-in may carry a matcher — as opposed to the * serializable `NavLinkSpec` a product authors. * * Re-exported rather than imported directly at each callsite so the ~40 modules * that already read nav types from here keep working, and so a future consumer * can find them where the nav data lives. */ export type { NavDrillInConfig, NavLinkItem, NavPrimaryLayout, NavSection, NavSecondaryItem, } from "@exxatdesignux/ui/components/shell" // ── Team / scope switcher mock data ────────────────────────────────────────── // // The sidebar header picker (TeamSwitcher) is **product-aware** per the // product-context rule (`.cursor/rules/exxat-product-context.mdc`): // // • Prism / One — Schools / Custom → school > program // • One — Sites → site > location // // Two parallel mock-data shapes live here — one for each scope hierarchy — // so the switcher can render either flavor without forking the chrome. // School + Program (school-side scope) ──────────────────────────────────────── export interface NavProgram { id: string; name: string } export interface NavSchool { id: string name: string /** URL of school logo — used as the avatar image */ logo: string /** Two-letter abbreviation shown as AvatarFallback */ initials: string programs: NavProgram[] } export const NAV_SCHOOLS: NavSchool[] = [ { id: "jhu", name: "Johns Hopkins University", logo: logoDevUrl("jhu.edu"), initials: "JH", programs: [ { id: "som", name: "Medicine" }, { id: "son", name: "Nursing" }, { id: "sph", name: "Public Health" }, ], }, { id: "mayo", name: "Mayo Clinic Alix School of Medicine", logo: logoDevUrl("mayoclinic.org"), initials: "MC", programs: [ { id: "md", name: "Doctor of Medicine" }, { id: "bms", name: "Biomedical Sciences" }, ], }, ] export const NAV_SCHOOL_DEFAULT = NAV_SCHOOLS[0] export const NAV_PROGRAM_DEFAULT = NAV_SCHOOLS[0].programs[0] // Site + Location (partner-side scope, Exxat One — Sites) ───────────────────── export interface NavLocation { id: string; name: string } export interface NavSite { id: string name: string /** URL of site logo — used as the avatar image */ logo: string /** Two-letter abbreviation shown as AvatarFallback */ initials: string locations: NavLocation[] } export const NAV_SITES: NavSite[] = [ { id: "mgb", name: "Mass General Brigham", logo: logoDevUrl("massgeneralbrigham.org"), initials: "MG", locations: [ { id: "mgh-main", name: "Mass General — Boston" }, { id: "bwh", name: "Brigham and Women's — Boston" }, { id: "mgh-salem", name: "Salem Hospital" }, ], }, { id: "ccf", name: "Cleveland Clinic", logo: logoDevUrl("clevelandclinic.org"), initials: "CC", locations: [ { id: "ccf-main", name: "Main Campus — Cleveland" }, { id: "ccf-fairview", name: "Fairview Hospital" }, ], }, ] export const NAV_SITE_DEFAULT = NAV_SITES[0] export const NAV_LOCATION_DEFAULT = NAV_SITES[0].locations[0] // ── Primary navigation ──────────────────────────────────────────────────────── // // The primary nav is **product-keyed** (`NAV_BY_PRODUCT[productId]`) per Rule // 5 of the multi-product routing pattern // (`apps/web/docs/multi-product-routing-pattern.md`). `AppSidebar` reads // `useProduct().product` and renders the matching tree. Switching products // in the sidebar swaps the entire primary nav. // // Product-owned hubs live under the active product root. Shell-global routes // like Help remain at the workspace root. const dashboardItem = (slug: string): NavLinkSpec => ({ key: "dashboard", title: "Dashboard", url: `/${slug}/dashboard`, iconClass: "fa-light fa-grid-2", iconActiveClass: "fa-solid fa-grid-2", }) const homeItem = (slug: string): NavLinkSpec => ({ key: "home", title: "Home", url: `/${slug}/dashboard`, iconClass: "fa-light fa-house", iconActiveClass: "fa-solid fa-house", }) function hubItem( slug: string, key: string, title: string, segment: string, iconClass: string, iconActiveClass: string, opts?: { badge?: number | string; secondaryPanel?: string }, ): NavLinkSpec { return { key, title, url: `/${slug}/${segment}`, iconClass, iconActiveClass, ...opts, } } function complianceItem(slug: string): NavLinkSpec { return { key: "compliance", title: "Compliance", url: `/${slug}/students`, iconClass: "fa-light fa-shield-check", iconActiveClass: "fa-solid fa-shield-check", primaryHubChildKey: "students", children: [ hubItem( slug, "students", "Students", "students", "fa-light fa-graduation-cap", "fa-solid fa-graduation-cap", ), hubItem( slug, "faculty-staff", "Faculty", "faculty-staff", "fa-light fa-user-group", "fa-solid fa-user-group", ), ], } } function flattenNavItems(items: NavLinkItem[]): NavLinkItem[] { const flat: NavLinkItem[] = [] for (const item of items) { flat.push(item) if (item.children?.length) flat.push(...flattenNavItems(item.children)) } return flat } /** * Prism + Custom primary nav — flat primary rows; Compliance nests Students + * Faculty. Question bank + DS pattern hubs live on Design OS (`buildDesignOsNavLayout`). */ function buildPrismNavLayout(slug: string): NavLayoutSpec { return { preamble: [ dashboardItem(slug), hubItem( slug, "program-details", "Program details", "program-details", "fa-light fa-landmark", "fa-solid fa-landmark", ), complianceItem(slug), hubItem(slug, "courses", "Courses", "courses", "fa-light fa-books", "fa-solid fa-books"), hubItem( slug, "curriculum-mapping", "Curriculum mapping", "curriculum-mapping", "fa-light fa-grid-2-plus", "fa-solid fa-grid-2-plus", { badge: "Leo" }, ), hubItem(slug, "sites", "Sites", "sites", "fa-light fa-building", "fa-solid fa-building"), hubItem( slug, "process-my-requests", "Process my Requests", "process-my-requests", "fa-light fa-file-magnifying-glass", "fa-solid fa-file-magnifying-glass", ), hubItem( slug, "placements", "Placements", "placements", "fa-light fa-user-plus", "fa-solid fa-user-plus", ), hubItem( slug, "learning-activities", "Learning activities", "learning-activities", "fa-light fa-clipboard-list", "fa-solid fa-clipboard-list", { secondaryPanel: "learning-activities" }, ), ], sections: [], } } /** * Exxat One — Sites primary nav — Home; Availability / Jobs / Site directory sections. * * Not in the switcher: the site-side app is kept whole, nav and routes included, * so its links keep resolving and it can come back without archaeology. */ function buildOneSitesNavLayout(slug: string): NavLayoutSpec { return { preamble: [homeItem(slug)], sections: [ { key: "availability-management", label: "Availability Management", items: [ hubItem(slug, "availability", "Availability", "availability", "fa-light fa-calendar", "fa-solid fa-calendar"), hubItem( slug, "slot-requests", "Slot Requests", "slot-requests", "fa-light fa-inbox", "fa-solid fa-inbox", ), hubItem(slug, "schedules", "Schedules", "schedules", "fa-light fa-calendar-lines", "fa-solid fa-calendar-lines"), hubItem(slug, "reports", "Reports", "reports", "fa-light fa-chart-line", "fa-solid fa-chart-line"), ], }, { key: "jobs-management", label: "Jobs Management", items: [ hubItem(slug, "jobs", "Jobs", "jobs", "fa-light fa-briefcase", "fa-solid fa-briefcase", { badge: "New" }), ], }, { key: "site-directory", label: "Site directory", items: [ hubItem(slug, "locations", "Locations", "locations", "fa-light fa-location-dot", "fa-solid fa-location-dot"), hubItem(slug, "personnel", "Personnel", "personnel", "fa-light fa-users", "fa-solid fa-users"), hubItem( slug, "school-partners", "School Partners", "school-partners", "fa-light fa-handshake", "fa-solid fa-handshake", ), ], }, ], } } /** * Administrator — workspace oversight only. People / Courses / Personnel are * separate Directory products with their own roots. */ function buildAdminNavLayout(slug: string): NavLayoutSpec { return { preamble: [ { key: "overview", title: "Overview", url: `/${slug}`, iconClass: "fa-light fa-gauge-high", iconActiveClass: "fa-solid fa-gauge-high", }, ], sections: [], } } function buildPeopleNavLayout(slug: string): NavLayoutSpec { return { preamble: [ { key: "students", title: "Students", url: `/${slug}/students`, iconClass: "fa-light fa-graduation-cap", iconActiveClass: "fa-solid fa-graduation-cap", }, { key: "faculty", title: "Faculty", url: `/${slug}/faculty`, iconClass: "fa-light fa-chalkboard-user", iconActiveClass: "fa-solid fa-chalkboard-user", }, { key: "staff", title: "Staff", url: `/${slug}/staff`, iconClass: "fa-light fa-user-tie", iconActiveClass: "fa-solid fa-user-tie", }, ], sections: [], } } function buildCoursesDirectoryNavLayout(slug: string): NavLayoutSpec { return { preamble: [ { key: "courses", title: "Course Management", url: `/${slug}`, iconClass: "fa-light fa-books", iconActiveClass: "fa-solid fa-books", }, ], sections: [], } } function buildProgramsDirectoryNavLayout(slug: string): NavLayoutSpec { return { preamble: [ { key: "programs", title: "Program Management", url: `/${slug}`, iconClass: "fa-light fa-sitemap", iconActiveClass: "fa-solid fa-sitemap", }, ], sections: [], } } function buildPersonnelDirectoryNavLayout(slug: string): NavLayoutSpec { return { preamble: [ { key: "personnel", title: "Personnel Management", url: `/${slug}`, iconClass: "fa-light fa-user-nurse", iconActiveClass: "fa-solid fa-user-nurse", }, ], sections: [], } } /** @deprecated Directory hubs are their own products — kept for redirect helpers. */ export const ADMIN_HUB_SEGMENTS = ["people", "courses", "personnel"] as const /** Route segments for One — Sites hub shells (excludes dashboard). */ export const ONE_SITES_HUB_SEGMENTS = [ "locations", "personnel", "school-partners", "availability", "slot-requests", "schedules", "jobs", ] as const /** Route segments for generic Prism hub shells (excludes dashboard + library). */ export const PRISM_HUB_SEGMENTS = [ "program-details", "students", "faculty-staff", "student-compliance", "faculty-compliance", "reports", "courses", "curriculum-mapping", "competency-management", "sites", "process-my-requests", "placements", "learning-activities", ] as const export function flattenNavLayout(layout: NavPrimaryLayout): NavLinkItem[] { const top = [ ...layout.preamble, ...layout.sections.flatMap((section) => section.items), ...(layout.epilogue ?? []), ] return flattenNavItems(top) } function buildPlaceholderLayout(slug: string): NavLayoutSpec { return { preamble: [dashboardItem(slug)], sections: [] } } /** * Announced but unbuilt products. They are switchable and routable so their * marketing pages and deep links resolve, but a nav full of links that go * nowhere is worse than one dashboard, so they share the placeholder layout. * Move a product out of this list when its hubs land. */ const UNBUILT_PRODUCTS = [ "exxat-curriculum-mapping", "exxat-compliance", "exxat-surveys", "exxat-exam-management", "exxat-accreditation", "exxat-student-success", ] as const satisfies readonly Product[] const UNBUILT_NAV_LAYOUTS = Object.fromEntries( UNBUILT_PRODUCTS.map(product => [product, buildPlaceholderLayout(productSlug(product))]), ) as Record<(typeof UNBUILT_PRODUCTS)[number], NavLayoutSpec> /** * Every product's primary nav, as **data** (ADR 0003 L4). Icons are Font Awesome * class strings, not elements, so a nav can be persisted, versioned, and diffed. * `navLayoutToJsx` turns a spec into rows the sidebar renders. */ export const NAV_LAYOUT_BY_PRODUCT: Record = { "exxat-prism": buildPrismNavLayout(productSlug("exxat-prism")), "exxat-design-os": buildDesignOsNavLayout(productSlug("exxat-design-os")), "exxat-admin": buildAdminNavLayout(productSlug("exxat-admin")), "exxat-people": buildPeopleNavLayout(productSlug("exxat-people")), "exxat-courses": buildCoursesDirectoryNavLayout(productSlug("exxat-courses")), "exxat-programs": buildProgramsDirectoryNavLayout(productSlug("exxat-programs")), "exxat-personnel": buildPersonnelDirectoryNavLayout(productSlug("exxat-personnel")), "exxat-one-schools": buildOneSchoolsNavLayout(productSlug("exxat-one-schools")), "exxat-one-sites": buildOneSitesNavLayout(productSlug("exxat-one-sites")), ...UNBUILT_NAV_LAYOUTS, "exxat-custom": buildPrismNavLayout("custom"), } export const NAV_BY_PRODUCT: Record = Object.fromEntries( Object.entries(NAV_LAYOUT_BY_PRODUCT).map(([product, layout]) => [ product, navToJsx(flattenNavLayoutSpec(layout)), ]), ) as Record const customNavSpecs = new Map() /** * Primary nav **spec** for the active product — custom slots use their suffix * slug (`/assessment/dashboard`, not `/custom/dashboard`). */ export function getPrimaryNavSpecForProduct( product: Product, customProducts: { suffix: string }[], activeCustomIndex: number, ): NavLayoutSpec { if (product === "exxat-custom") { const brand = customProducts[activeCustomIndex] ?? customProducts[0] const slug = brand?.suffix?.trim() ? customProductSlugFromSuffix(brand.suffix) : "custom" // Tenant custom products (Settings → Add product) inherit Prism primary IA // under `//…`. Registered tenant nav stubs (dashboard + // library + placements) are for the generic shell — not the full tree. const cached = customNavSpecs.get(slug) if (cached) return cached const spec = buildPrismNavLayout(slug) customNavSpecs.set(slug, spec) if (customNavSpecs.size > 100) { const oldest = customNavSpecs.keys().next().value if (oldest) customNavSpecs.delete(oldest) } return spec } return NAV_LAYOUT_BY_PRODUCT[product] } /** * Rendered form of a spec, cached per spec object. * * `AppSidebar` calls the accessors below on every render, so converting eagerly * would hand the nav components a fresh row object (and fresh icon element) each * time and defeat their memoization. Keyed on the spec identity, which is stable * for every product whose layout lives in `NAV_LAYOUT_BY_PRODUCT`. */ const renderedLayouts = new WeakMap() function renderNavLayout(spec: NavLayoutSpec): NavPrimaryLayout { const cached = renderedLayouts.get(spec) if (cached) return cached const rendered = navLayoutToJsx(spec) renderedLayouts.set(spec, rendered) return rendered } export function getPrimaryNavLayoutForProduct( product: Product, customProducts: { suffix: string }[], activeCustomIndex: number, ): NavPrimaryLayout { return renderNavLayout( getPrimaryNavSpecForProduct(product, customProducts, activeCustomIndex), ) } export function getPrimaryNavForProduct( product: Product, customProducts: { suffix: string }[], activeCustomIndex: number, ): NavLinkItem[] { return flattenNavLayout( getPrimaryNavLayoutForProduct(product, customProducts, activeCustomIndex), ) } /** * @deprecated Prefer `NAV_BY_PRODUCT[product]` with the active product from * `useProduct()`. This export remains for any callsite that hasn't migrated * yet — it points at Prism's primary nav (the only product whose IA is * fully built today). Will be removed once all callers move to the * product-keyed registry. */ export const NAV_PRIMARY: NavLinkItem[] = NAV_BY_PRODUCT["exxat-prism"] // ── Documents section ─────────────────────────────────────────────────────── export const NAV_DOCUMENTS_LABEL = "Resources" /** * DS pattern hubs (Library, tokens, columns) live on the internal * `exxat-design-os` product primary nav — not in the shared Resources strip. */ export const NAV_DOCUMENTS: NavLinkItem[] = [] // ── Quick actions (above primary nav) + bottom utilities ─────────────────────── /** * Classic sidebar quick actions + Ask Leo drill-in config. Utility-bar layouts * use `UtilityBarSlot` instead; this array still powers `sidebar-classic` and * the `/leo` drill-in section search in `app-sidebar.tsx`. */ export const NAV_QUICK_ACTIONS: NavSecondaryItem[] = [ { key: "ask-leo", title: "Ask Leo", url: "#", icon: