Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions content/docs/components/navigation/navbar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,32 +112,33 @@ export function SiteNav() {
onValueChange={setLocale}
cta={{ label: "Get Started", href: "/signup" }}
showGitHub
githubUrl="your-org/your-repo" // or a full URL
/>
);
}
```

## Props

| Prop | Type | Required | Description |
| --------------- | --------------------------------- | -------- | ---------------------------------------------------------------- |
| `logo` | `React.ReactNode` | No | Logo element (renders before nav links) |
| `links` | `NavLink[]` | No | Navigation links |
| `activeHref` | `string` | No | `href` of the current page — gets `aria-current` and a highlight |
| `locales` | `Locale[]` | No | Available locales for language selector |
| `value` | `string` | No | Selected locale code — pass this to control the component |
| `defaultValue` | `string` | No | Initial locale code when uncontrolled (default: first locale) |
| `onValueChange` | `(code: string) => void` | No | Callback when locale changes |
| `cta` | `{ label: string; href: string }` | No | CTA button |
| `showGitHub` | `boolean` | No | Show GitHub icon button |
| `githubUrl` | `string` | No | GitHub repository URL |
| `sticky` | `boolean` | No | Make navbar sticky at top (default: `true`) |
| `variant` | `"default" \| "floating"` | No | Full-width bar or inset floating card (default: `"default"`) |
| `showFlags` | `boolean` | No | Show flags in language selector (default: `true`) |
| `maxHeight` | `number \| string` | No | Max height of the scrolling language menu (default: `"15rem"`) |
| `linkComponent` | `React.ElementType` | No | Component used for internal links (default: `"a"`) |
| `classNames` | `NavBarClassNames` | No | Per-part class overrides (see below) |
| `className` | `string` | No | Additional CSS classes |
| Prop | Type | Required | Description |
| --------------- | --------------------------------- | -------- | ----------------------------------------------------------------------------- |
| `logo` | `React.ReactNode` | No | Logo element (renders before nav links) |
| `links` | `NavLink[]` | No | Navigation links |
| `activeHref` | `string` | No | `href` of the current page — gets `aria-current` and a highlight |
| `locales` | `Locale[]` | No | Available locales for language selector |
| `value` | `string` | No | Selected locale code — pass this to control the component |
| `defaultValue` | `string` | No | Initial locale code when uncontrolled (default: first locale) |
| `onValueChange` | `(code: string) => void` | No | Callback when locale changes |
| `cta` | `{ label: string; href: string }` | No | CTA button |
| `showGitHub` | `boolean` | No | Show the GitHub icon button. Needs `githubUrl` — without one, nothing renders |
| `githubUrl` | `string` | No | Full URL, or `"owner"` / `"owner/repo"` shorthand for github.com |
| `sticky` | `boolean` | No | Make navbar sticky at top (default: `true`) |
| `variant` | `"default" \| "floating"` | No | Full-width bar or inset floating card (default: `"default"`) |
| `showFlags` | `boolean` | No | Show flags in language selector (default: `true`) |
| `maxHeight` | `number \| string` | No | Max height of the scrolling language menu (default: `"15rem"`) |
| `linkComponent` | `React.ElementType` | No | Component used for internal links (default: `"a"`) |
| `classNames` | `NavBarClassNames` | No | Per-part class overrides (see below) |
| `className` | `string` | No | Additional CSS classes |

Any other prop is spread onto the underlying `<nav>` element, and the ref is
forwarded to it.
Expand Down Expand Up @@ -244,6 +245,7 @@ export default function App() {
}}
cta={{ label: "Get Started", href: "/signup" }}
showGitHub
githubUrl="your-org/your-repo"
/>
);
}
Expand Down Expand Up @@ -329,6 +331,18 @@ export default function App() {
}
```

### GitHub link

`showGitHub` adds an icon button that links to `githubUrl`. Pass `"owner/repo"` for a
repository, `"owner"` for a profile or organisation, or any full URL.

```tsx
<NavBar showGitHub githubUrl="acme/website" />
<NavBar showGitHub githubUrl="https://github.com/acme" />
```

`showGitHub` without `githubUrl` renders no button and logs a warning.

### Non-sticky

```tsx
Expand Down
4 changes: 2 additions & 2 deletions content/docs/customization/interfaces.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ export interface NavBarProps {
linkComponent?: React.ElementType;
/** CTA button */
cta?: { label: string; href: string };
/** Show GitHub icon button */
/** Show GitHub icon button. Needs `githubUrl`; without one, nothing renders */
showGitHub?: boolean;
/** GitHub repository URL */
/** Full URL, or "owner" / "owner/repo" shorthand for github.com */
githubUrl?: string;
/** Make navbar sticky at top (default: true) */
sticky?: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/components/docs/navbar-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function NavBarDemo() {
onValueChange={setLocale}
cta={{ label: navCopy[locale].cta, href: "#get-started" }}
showGitHub
githubUrl="babelize/babelize-elements" // "owner/repo" or a full URL
variant="floating" // default, floating
sticky={false}
/>
Expand Down
42 changes: 35 additions & 7 deletions src/registry/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ export interface NavBarProps extends Omit<
onValueChange?: (code: string) => void;
/** Primary call-to-action button */
cta?: { label: string; href: string };
/** Show a GitHub icon link */
/** Show a GitHub icon link. Needs `githubUrl`; without one, nothing renders. */
showGitHub?: boolean;
/** Target of the GitHub icon link */
/**
* Target of the GitHub icon link: a full URL, or `"owner"` / `"owner/repo"`
* shorthand for a github.com profile or repository.
*/
githubUrl?: string;
/** Pin the bar to the top of the viewport (default: true) */
sticky?: boolean;
Expand Down Expand Up @@ -131,6 +134,18 @@ const GHOST_BUTTON = cn(

const EASE = [0.16, 1, 0.3, 1] as const;

/**
* Expands `"owner"` / `"owner/repo"` shorthand to a github.com URL. Anything else,
* full URLs included, passes through unchanged.
*/
function resolveGitHubUrl(value: string | undefined): string | undefined {
const trimmed = value?.trim();
if (!trimmed) return undefined;
if (/^[a-z][a-z\d+.-]*:/i.test(trimmed)) return trimmed;
const path = trimmed.replace(/^(www\.)?github\.com\//i, "").replace(/^\/+|\/+$/g, "");
return /^[A-Za-z\d-]+(\/[\w.-]+)?$/.test(path) ? `https://github.com/${path}` : trimmed;
}

export const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar(
{
logo,
Expand All @@ -142,7 +157,7 @@ export const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar
onValueChange,
cta,
showGitHub = false,
githubUrl = "https://github.com/babelize/babelize-elements",
githubUrl,
sticky = true,
variant = "default",
className,
Expand Down Expand Up @@ -308,9 +323,22 @@ export const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar
</Panel>
);

const githubHref = resolveGitHubUrl(githubUrl);
// `showGitHub` alone used to link to this library's own repository. Now it
// renders nothing, so say why instead of failing silently.
const missingGitHubUrl = showGitHub && !githubHref;
React.useEffect(() => {
if (missingGitHubUrl) {
console.warn(
'NavBar: `showGitHub` needs `githubUrl` (a URL or "owner/repo"); the GitHub link is hidden.',
);
}
}, [missingGitHubUrl]);
const hasGitHub = showGitHub && !!githubHref;

const githubLink = (
<a
href={githubUrl}
href={githubHref}
target="_blank"
rel="noopener noreferrer"
className={cn(GHOST_BUTTON, "size-9")}
Expand Down Expand Up @@ -436,7 +464,7 @@ export const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar
</div>
)}

{showGitHub && githubLink}
{hasGitHub && githubLink}

{cta && (
<Link href={cta.href} className={cn(ctaClass, "ms-1")}>
Expand Down Expand Up @@ -539,7 +567,7 @@ export const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar
</Link>
</motion.div>
))}
{(cta || showGitHub) && (
{(cta || hasGitHub) && (
<motion.div
className="mt-2 flex items-center gap-2 border-t pt-3"
initial={{ opacity: 0, y: -6 }}
Expand All @@ -556,7 +584,7 @@ export const NavBar = React.forwardRef<HTMLElement, NavBarProps>(function NavBar
{ctaArrow}
</Link>
)}
{showGitHub && githubLink}
{hasGitHub && githubLink}
</motion.div>
)}
</div>
Expand Down
28 changes: 28 additions & 0 deletions test/navbar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,31 @@ describe("NavBar customization", () => {
expect(screen.getAllByRole("link", { name: "Docs" })).toHaveLength(1);
});
});

describe("NavBar GitHub link", () => {
const githubLinks = () => screen.queryAllByRole("link", { name: "GitHub" });

it.each([
["acme/website", "https://github.com/acme/website"],
["acme", "https://github.com/acme"],
["github.com/acme/website/", "https://github.com/acme/website"],
["https://github.com/acme/website", "https://github.com/acme/website"],
["https://gitlab.com/acme/website", "https://gitlab.com/acme/website"],
])("resolves githubUrl=%s to %s", (githubUrl, href) => {
render(<NavBar showGitHub githubUrl={githubUrl} />);
expect(githubLinks()[0]).toHaveAttribute("href", href);
});

it("renders nothing and warns when showGitHub has no githubUrl", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
render(<NavBar showGitHub />);
expect(githubLinks()).toHaveLength(0);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("`showGitHub` needs `githubUrl`"));
warn.mockRestore();
});

it("stays hidden without showGitHub, even with a githubUrl", () => {
render(<NavBar githubUrl="acme/website" />);
expect(githubLinks()).toHaveLength(0);
});
});
Loading