Files
supabase/apps/docs/components/Navigation/NavigationMenu/GlobalNavigationMenu.tsx
T
Ivan VasilovandClaude Opus 4.7 72f57dde89 chore: migrate remaining opacity utilities to slash syntax
Finishes the `*-opacity-*` → slash-modifier conversion that the
`@tailwindcss/upgrade` tool couldn't complete because the paired
color/opacity classes weren't always adjacent (intervening tokens,
variant prefixes, or absent base color).

Mechanical same-line paired rewrites (perl):
  bg-{color} bg-opacity-N           → bg-{color}/N
  bg-opacity-N bg-{color}           → bg-{color}/N   (reversed order)
  bg bg-opacity-N                   → bg/N           (bare `bg` alias)
  (analogous for border-, ring-, divide-)

Hand-edited ambiguous stragglers:

- Rewrote `{variant}:{prefix}-opacity-N` to `{variant}:{prefix}-{base-color}/N`
  where a base color was present on the same element (TriggerSheet,
  ExitSurveyModal, UpgradeModal, DeleteProjectModal, CommandMenu,
  MultiSelectDeprecated).
- Dropped no-op `bg-opacity-100` / `hover:bg-opacity-100` where the
  companion rule already implied full opacity (BillingChangeBadge,
  SupportAccessToggle, MobileNavigationBar, Announcement/Badge,
  LWAnnouncement, Button link variant).
- Rewrote `hover:border-opacity-30` paired with a later base color to
  `hover:border-foreground-muted/30` in Announcement/Badge.
- Reworked ComputeBadge state-open styles: `group-data-[state=open]:
  bg-opacity-20` / `ring-2/20` (invalid) → `group-data-[state=open]:
  bg-{color}/20 ring-{color}/20` mapped per `smallCompute` branch.
- Migrated `defaultTheme.ts` badge theme: dropped `bg-opacity-10` from
  base, applied `/10` to each color variant's bg. Deleted dead `utils`
  block (declared but never referenced/exported). Fixed stray `bg-200`
  typo → `bg-gray-200/10`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> (+16 squashed commits)
Squashed commits:
[3f18aa64d5] chore(packages/marketing): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `packages/marketing/`, using the same temporary
`_upgrade-entry.css` pattern. Spurious tailwindcss dep addition was
reverted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[dfd9039173] chore(packages/dev-tools): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `packages/dev-tools/`, using the same temporary `_upgrade-entry.css`
pattern as the other shared packages. The spurious tailwindcss dep
addition was reverted — this package is a consumer, not a Tailwind
owner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[f815ff2905] chore(packages/ui-patterns): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `packages/ui-patterns/`. Same temporary `_upgrade-entry.css`
pattern as the `packages/ui` run — added to give the tool a CSS entry
to scan templates against, removed before committing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[999168249a] chore(packages/ui): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `packages/ui/`. A temporary `_upgrade-entry.css` was added during
the run (with `@import "tailwindcss"` + `@config` bridge to the shared
config) so the tool could discover a CSS entry and drive template
migration across `src/**/*.tsx`; it's removed before committing.

Per-app upgrade runs skip `packages/ui` as "outside repository", so this
is the only way to cover it without running the tool at the workspace
root.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[32cdf2253a] chore(ui-library): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/ui-library/`. Also bundles minor `pnpm-lock.yaml` churn
(peer-dep reshuffling triggered by the tool's install pass).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[bf54bac2f1] chore(lite-studio): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/lite-studio/`. The first attempt failed on an unknown
utility (`text-foreground-light`) because `typography.css` loaded its
own Tailwind context without access to the shared plugin. After
switching to `@reference './app.css'` (the same pattern used in
`apps/studio/styles/typography.css`), the tool ran cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[e7373cf938] chore(www): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/www/`. Also:

- Bumps `apps/www/package.json` tailwindcss from `^3.4.1` to `^4.2.4`
  (www had an explicit dep rather than the catalog reference).
- Fixes a double-slash path glitch the tool emitted in
  `apps/www/styles/index.css` (`@config '..//tailwind.config.js'` →
  `@config '../tailwind.config.js'`).
- `pnpm-lock.yaml` bundled here as it reflects cumulative dep changes
  from www plus the earlier `apps/learn` addition.

The tool warned that `apps/www/postcss.config.js` contains dynamic
JavaScript and was not auto-migrated — that file needs a manual pass
in a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[9fcbb5f28c] chore(studio): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/studio/`. Biggest diff of any single app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[e77b3b9a27] chore(learn): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/learn/`. Also adds `tailwindcss: ^4.2.4` to the app's
package.json (the upgrade tool injects this when the app didn't
previously declare tailwindcss directly).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[5ad834d688] chore(docs): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/docs/`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[f3101bf306] chore(design-system): run @tailwindcss/upgrade v4 codemods

Automated output from `pnpm dlx @tailwindcss/upgrade --force` executed
within `apps/design-system/`. Handles deprecated-class renames, `@apply`
prefix-bang → suffix-bang syntax, gradient utility renames, and related
v4-aware rewrites. Only files under `apps/design-system/` are touched;
shared packages are skipped by the tool when run from a single app.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[f60e5d01d1] chore: pin tailwindcss to 4.2.4 in all package.json files

Replace `"tailwindcss": "catalog:"` and `"@tailwindcss/postcss": "catalog:"`
with explicit `4.2.4` pins so `@tailwindcss/upgrade` can run without a
catalog-vs-node_modules version mismatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[0676c22a12] chore: rename deprecated Tailwind utility classes

Apply the non-opacity class renames from Tailwind v3.4 → v4, across all
source files (`*.tsx`, `*.ts`, `*.jsx`, `*.js`, `*.css`, `*.mdx`),
excluding `examples/` and the vendored `apps/studio/public/monaco-editor`
bundle.

- `flex-grow` → `grow`
- `flex-shrink` → `shrink`
- `overflow-ellipsis` → `text-ellipsis`
- `shadow-sm` → `shadow-xs`
- `rounded-sm` → `rounded-xs`
- `blur-sm` → `blur-xs`
- `outline-none` → `outline-hidden` (preserves v3's accessibility-friendly
  behavior; v4's `outline-none` now means `outline-style: none` only)

CSS property syntax (`flex-grow: 1;`, `outline: none;`, etc.) was
protected with a negative lookahead for `\s*:` so raw CSS is untouched.

Opacity utilities (`bg-opacity-*`, `border-opacity-*`, `ring-opacity-*`,
`divide-opacity-*`) and `ring` → `ring-3` are context-dependent and are
deferred to a follow-up commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[3d8e96069e] Bunch of fixes.
[9dadccf323] Migrate studio.

Co-authored-by: Copilot <copilot@github.com>
[9e706b3092] chore(config): bump Tailwind to v4 via @config bridge

- Bump catalog `tailwindcss` from 3.4.1 to 4.2.3 and add
  `@tailwindcss/postcss` 4.2.3.
- Swap shared PostCSS config to `@tailwindcss/postcss` and drop
  `tailwindcss/nesting` (v4 handles nesting internally via Lightning
  CSS).
- Replace `@tailwind base/components/utilities` with
  `@import "tailwindcss"` + `@config "../../packages/config/tailwind.config.js"`
  in the 10 app-level CSS entry files (studio, www, docs, design-system,
  ui-library, learn, lite-studio). Skips `examples/*` which aren't part
  of the monorepo build.
- Remove `<alpha-value>` placeholders from
  `packages/config/tailwind.config.js` (4 occurrences) and from
  `apps/www/tailwind.config.js` (purple-sos color scale, 9 occurrences).
  Trade-off: opacity modifiers like `bg-studio/50` no longer work for
  these theme colors — they're used with static classes today.
- Remove `@tailwindcss/container-queries` — it's built into v4. Dropped
  from `apps/studio`, `apps/design-system`, `apps/docs` package.jsons and
  their Tailwind configs.

Custom plugins (`hit-area`, `motion-safe-transition`), `tailwindcss-radix`,
`@mertasan/tailwindcss-variables`, and `tailwindcss-animate` are kept
as-is; the v3-style `plugin()` / `matchUtilities()` / `addVariant()` APIs
still work under v4. Swapping `hit-area` to the v4-native CSS `@utility`
version and replacing `tailwindcss-radix` with native `data-[state=*]:`
variants is deferred to follow-up PRs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:19:37 +02:00

193 lines
7.6 KiB
TypeScript

'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import React, { FC, Fragment, useEffect, useState } from 'react'
import {
Badge,
cn,
MenubarSeparator,
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from 'ui'
import MenuIconPicker from './MenuIconPicker'
import { GLOBAL_MENU_ITEMS } from './NavigationMenu.constants'
/**
* Get TopNav active label based on current pathname
*/
export const useActiveMenuLabel = (menu: typeof GLOBAL_MENU_ITEMS) => {
const pathname = usePathname()
const [activeLabel, setActiveLabel] = useState('')
useEffect(() => {
// check if homepage
if (pathname === '/') {
return setActiveLabel('Home')
}
for (let index = 0; index < menu.length; index++) {
const section = menu[index]
if (section[0].enabled === false) continue
// check if first level menu items match beginning of url
if (section[0].href?.startsWith(pathname)) {
return setActiveLabel(section[0].label)
}
// check if second level menu items match beginning of url
if (section[0].menuItems) {
section[0].menuItems.map((menuItemGroup) =>
menuItemGroup
.filter((menuItem) => menuItem.enabled !== false)
.map(
(menuItem) => menuItem.href?.startsWith(pathname) && setActiveLabel(section[0].label)
)
)
}
}
}, [pathname, menu])
return activeLabel
}
const GlobalNavigationMenu: FC = () => {
const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS)
const triggerClassName =
'h-(--header-height) p-2 bg-transparent border-0 border-b-2 border-transparent font-normal rounded-none text-foreground-light hover:text-foreground data-open:text-foreground! data-radix-collection-item:focus-visible:ring-2 data-radix-collection-item:focus-visible:ring-foreground-lighter data-radix-collection-item:focus-visible:text-foreground h-full focus-visible:rounded-sm shadow-none! outline-hidden transition-all outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600'
return (
<div className="flex relative gap-2 justify-start items-end w-full h-full">
<NavigationMenu
delayDuration={0}
skipDelayDuration={0}
className="w-full flex justify-start h-full"
renderViewport={false}
viewportClassName="mt-0 max-w-screen overflow-hidden border-0 rounded-none mt-1.5 rounded-md border-x!"
>
<NavigationMenuList className="px-6 space-x-2 h-(--header-height)">
{GLOBAL_MENU_ITEMS.filter((section) => section[0].enabled !== false).map(
(section, sectionIdx) =>
section[0].menuItems ? (
<NavigationMenuItem
key={`desktop-docs-menu-section-${section[0].label}-${sectionIdx}`}
className="text-sm relative h-full"
>
<NavigationMenuTrigger
className={cn(
navigationMenuTriggerStyle(),
triggerClassName,
activeLabel === section[0].label && 'text-foreground border-foreground'
)}
>
{section[0].label === 'Home' ? (
<MenuIconPicker icon={section[0].icon || ''} />
) : (
section[0].label
)}
</NavigationMenuTrigger>
<NavigationMenuContent className="top-[calc(100%+4px)]! min-w-56 max-h-[calc(100vh-4rem)] border-y w-screen md:w-64 overflow-hidden overflow-y-auto rounded-none md:rounded-md md:border border-overlay bg-overlay text-foreground-light shadow-md duration-0!">
<div className="p-3 md:p-1">
{section[0].menuItems?.map((menuItem, menuItemIndex) => (
<Fragment
key={`desktop-docs-menu-section-${menuItemIndex}-${menuItemIndex}`}
>
{menuItemIndex !== 0 && <MenubarSeparator className="bg-border-muted" />}
{menuItem
.filter((item) => item.enabled !== false)
.map((item, itemIdx) =>
!item.href ? (
<div
key={`desktop-docs-menu-section-label-${item.label}-${itemIdx}`}
className="font-mono tracking-wider flex items-center text-foreground-muted text-xs uppercase rounded-md p-2 leading-none"
>
{item.label}
</div>
) : (
<NavigationMenuLink
key={`desktop-docs-menu-section-label-${item.label}-${itemIdx}`}
asChild
>
<MenuItem
href={item.href}
title={item.label}
community={item.community}
icon={item.icon}
/>
</NavigationMenuLink>
)
)}
</Fragment>
))}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuItem
key={`desktop-docs-menu-section-${section[0].label}-${sectionIdx}`}
className="text-sm relative h-full"
>
<NavigationMenuLink asChild>
<Link
href={section[0].href || '#'}
className={cn(
navigationMenuTriggerStyle(),
triggerClassName,
activeLabel === section[0].label && 'text-foreground border-foreground'
)}
>
{section[0].label === 'Home' ? (
<MenuIconPicker icon={section[0].icon || ''} />
) : (
section[0].label
)}
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
)
)}
</NavigationMenuList>
</NavigationMenu>
</div>
)
}
export const MenuItem = React.forwardRef<
React.ElementRef<'a'>,
React.ComponentPropsWithoutRef<'a'> & {
icon?: string
community?: boolean
}
>(({ className, title, href = '', icon, community, children, ...props }, ref) => {
return (
<Link
href={href}
ref={ref}
className={cn(
'group/menu-item flex items-center gap-2',
'w-full flex h-8 items-center text-foreground-light text-sm hover:text-foreground select-none rounded-md p-2 leading-none no-underline outline-hidden! focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:text-foreground',
className
)}
{...props}
>
{children ?? (
<>
{icon && <MenuIconPicker icon={icon} className="text-foreground-lighter" />}
<span className="flex-1">{title}</span>
{community && <Badge>Community</Badge>}
</>
)}
</Link>
)
})
GlobalNavigationMenu.displayName = 'GlobalNavigationMenu'
MenuItem.displayName = 'MenuItem'
export default GlobalNavigationMenu