{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "main-nav", "title": "Main Navigation", "description": "The full-width NSW site navigation bar with mega-menu panels, current-page marking and 13 token-based surface colours, built on the navigation-menu component.", "dependencies": [ "@base-ui/react", "class-variance-authority", "clsx", "tailwind-merge" ], "registryDependencies": [ "https://ui.digital.nsw.gov.au/registry/r/theme.json", "https://ui.digital.nsw.gov.au/registry/r/link.json", "https://ui.digital.nsw.gov.au/registry/r/icons.json" ], "files": [ { "path": "src/components/main-nav.tsx", "content": "'use client'\n\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport React from 'react'\n\nimport { IconEast } from '@/icons/east'\nimport { cn } from '@/lib/utils'\n\nimport {\n NavigationMenu,\n NavigationMenuContent,\n NavigationMenuItem,\n NavigationMenuLink,\n NavigationMenuList,\n NavigationMenuTrigger,\n} from '@/components/navigation-menu'\n\n/**\n * Surface colours the main navigation can be themed with — exactly the\n * Footer's thirteen-name vocabulary, so a service that themes its footer\n * `primary-800` writes the same word here. Each name is the LIGHT-mode\n * surface; dark mode deepens it onto the same family's dark steps (see the\n * `color` variants below), so `color` expresses tonal weight rather than a\n * literal colour.\n *\n * The bar's text/background pairs are identical to the Footer's, so its\n * contrast conclusions carry over verbatim: every light pair clears WCAG 2.2\n * AA (1.4.3, 4.5:1) for the bar's text and 11 of the 13 also clear AAA\n * (1.4.6, 7:1). The two AA-only pairs are `primary-600` (4.57:1) and\n * `accent-600` (5.18:1), both with white ink; prefer the `-800` steps when a\n * service is held to AAA. In dark mode all thirteen clear AAA (worst 13.6:1).\n *\n * The mega panels always render on the house popup surface (`bg-popover`),\n * matching the nswds-app source where every dropdown was white regardless of\n * bar colour — only the panel's accent ink follows the colour family.\n */\nconst mainNavColors = [\n 'primary-800',\n 'primary-600',\n 'primary-400',\n 'primary-200',\n 'grey-800',\n 'grey-600',\n 'grey-400',\n 'grey-200',\n 'accent-800',\n 'accent-600',\n 'accent-400',\n 'accent-200',\n 'white',\n] as const\n\ntype MainNavColor = (typeof mainNavColors)[number]\n\n// Shared by cva's defaultVariants, the data-color attribute and the panel\n// variant lookup, so the three can't drift apart.\nconst DEFAULT_MAIN_NAV_COLOR: MainNavColor = 'white'\n\nconst mainNavVariants = cva(\n [\n 'w-full',\n // Every derived colour on the bar resolves from a single --main-nav-ink\n // token, so a colour variant only has to declare the surface utilities and\n // that one value — this replaces the nswds-app source's hand-written\n // 13-variant × 8-slot style table. The border derivation is written\n // literally here (once) rather than per-variant: Tailwind scans source\n // text for class names, so a template-built arbitrary property would\n // never be emitted. Hover/active halos are not derived here — the\n // triggers ride navigation-menu.tsx's --nav-menu-halo chain, re-pointed\n // at this ink (see mainNavItemInk), so there is exactly one halo recipe.\n '[--main-nav-border:color-mix(in_oklch,var(--main-nav-ink)_15%,transparent)]',\n ],\n {\n variants: {\n // Ink values use the RAW masterbrand tokens (--primary-800, --grey-800,\n // --accent-800) rather than Tailwind's --color-* bridge aliases. Tailwind\n // v4 tree-shakes an unreferenced @theme key, and referencing one from\n // inside an arbitrary property is not a usage signal — the raw tokens are\n // plain :root declarations from @nswds/tokens and always resolve.\n // (--color-white is safe: `text-white` / `bg-white` below are real\n // utilities, so that key is always emitted.)\n //\n // Dark mode deepens every surface onto the same family's dark steps —\n // -800→-950, -600→-900, -400→-850, -200→-800, white→grey-900 — the\n // Footer's exact mapping, so the luminance ORDER within a family is\n // preserved and a stacked Footer + MainNav sharing one colour word stay\n // in step in both themes. Ink goes white on all thirteen; every dark\n // pair is WCAG 2.2 AAA (worst 13.6:1).\n color: {\n 'primary-800':\n 'bg-primary-800 text-white [--main-nav-ink:var(--color-white)] dark:bg-primary-950',\n 'primary-600':\n 'bg-primary-600 text-white [--main-nav-ink:var(--color-white)] dark:bg-primary-900',\n 'primary-400':\n 'bg-primary-400 text-primary-800 [--main-nav-ink:var(--primary-800)] dark:bg-primary-850 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n 'primary-200':\n 'bg-primary-200 text-primary-800 [--main-nav-ink:var(--primary-800)] dark:bg-primary-800 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n 'grey-800': 'bg-grey-800 text-white [--main-nav-ink:var(--color-white)] dark:bg-grey-950',\n 'grey-600': 'bg-grey-600 text-white [--main-nav-ink:var(--color-white)] dark:bg-grey-900',\n 'grey-400':\n 'bg-grey-400 text-grey-800 [--main-nav-ink:var(--grey-800)] dark:bg-grey-850 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n 'grey-200':\n 'bg-grey-200 text-grey-800 [--main-nav-ink:var(--grey-800)] dark:bg-grey-800 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n 'accent-800':\n 'bg-accent-800 text-white [--main-nav-ink:var(--color-white)] dark:bg-accent-950',\n 'accent-600':\n 'bg-accent-600 text-white [--main-nav-ink:var(--color-white)] dark:bg-accent-900',\n 'accent-400':\n 'bg-accent-400 text-accent-800 [--main-nav-ink:var(--accent-800)] dark:bg-accent-850 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n 'accent-200':\n 'bg-accent-200 text-accent-800 [--main-nav-ink:var(--accent-800)] dark:bg-accent-800 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n white:\n 'bg-white text-grey-800 [--main-nav-ink:var(--grey-800)] dark:bg-grey-900 dark:text-white dark:[--main-nav-ink:var(--color-white)]',\n },\n // Rules along the bar's edges, drawn from the ink-derived border token so\n // they follow every colour variant and both themes. Replaces the app\n // source's deprecated showTopBorder/showBottomBorder booleans, which are\n // deliberately not ported.\n border: {\n none: '',\n top: 'border-t border-(--main-nav-border)',\n bottom: 'border-b border-(--main-nav-border)',\n both: 'border-y border-(--main-nav-border)',\n },\n },\n defaultVariants: {\n color: DEFAULT_MAIN_NAV_COLOR,\n border: 'none',\n },\n },\n)\n\nconst mainNavContainerVariants = cva(\n [\n 'mx-auto flex w-full items-center',\n // Lateral padding funnels through --main-nav-padding-x so an app can\n // retune it once (via the `style` prop or a utility class) without a new\n // variant. Same mechanism, and the same 16px → 24px → 48px rhythm, as\n // Masthead, Header and Footer.\n 'px-(--main-nav-padding-x)',\n '[--main-nav-padding-x:--spacing(4)] sm:[--main-nav-padding-x:--spacing(6)] lg:[--main-nav-padding-x:--spacing(12)]',\n ],\n {\n variants: {\n container: {\n // Full-bleed (nswds-app fullBleed parity). --main-nav-max-width is\n // still read so a shell app can constrain the inner wrapper without\n // switching variants.\n fluid: 'max-w-[var(--main-nav-max-width,none)]',\n // Centred content column, legacy nsw-container parity (1200px).\n contained: 'max-w-[var(--main-nav-max-width,75rem)]',\n },\n },\n defaultVariants: {\n container: 'fluid',\n },\n },\n)\n\n/**\n * Mega-panel accent ink, keyed by the same thirteen colour names as the bar.\n * The panel itself is always the house popup surface (`bg-popover`, white in\n * light mode) — parity with the app source, whose dropdown was white for\n * every variant — so the family only tints the featured lead link, hover ink\n * and cell borders. Declared on the Content element (not inherited from the\n * bar) because Base UI portals the panel to `document.body`: a custom\n * property set on the `