| 1 | import { type VariantProps, cva } from "class-variance-authority"; |
| 2 | import type { ComponentProps, ReactNode } from "react"; |
| 3 | import { Link } from "react-router"; |
| 4 | |
| 5 | import { cn } from "../../lib/cn"; |
| 6 | import { SoonPill } from "./index"; |
| 7 | import { TabStrip } from "./tab-strip"; |
| 8 | |
| 9 | // Navigation tabs: links that say which page or view you are on, in a row |
| 10 | // that scrolls sideways on a phone (./tab-strip). shadcn's Tabs switch panels |
| 11 | // on one page; these go to other addresses. Two variants, one per level: |
| 12 | // |
| 13 | // `line` is the primary row along a header's bottom border: the current tab |
| 14 | // is underlined on that border, and the row sits one pixel into it so the |
| 15 | // two lines are one. `pill` is a secondary row inside a page's content, for |
| 16 | // the views of one section: the current view a quiet raised chip, nothing |
| 17 | // drawn outside the tab's own box, so the strip's overflow never cuts it. |
| 18 | |
| 19 | export const navTabsVariants = cva("", { |
| 20 | variants: { |
| 21 | variant: { |
| 22 | // Over the border: the strip's bottom edge is the header's line. The |
| 23 | // first tab's text lines up with the page's gutter, its underline a |
| 24 | // hair past it, and on a phone either row runs to the screen's edges |
| 25 | // as it scrolls. |
| 26 | line: "-mx-4 -mb-px gap-4 px-3 sm:-mx-1 sm:px-0", |
| 27 | pill: "-mx-4 gap-1 px-4 sm:mx-0 sm:px-0", |
| 28 | }, |
| 29 | }, |
| 30 | defaultVariants: { variant: "line" }, |
| 31 | }); |
| 32 | |
| 33 | export const navTabVariants = cva( |
| 34 | "flex items-center gap-1.5 text-sm whitespace-nowrap outline-none transition-colors focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset [&_svg]:shrink-0", |
| 35 | { |
| 36 | variants: { |
| 37 | variant: { |
| 38 | line: "h-11 border-b-2 border-transparent px-1 hover:border-line-strong aria-[current=page]:border-accent", |
| 39 | pill: "h-8 rounded-md px-3 hover:bg-raised/60 aria-[current=page]:bg-raised", |
| 40 | }, |
| 41 | soon: { |
| 42 | true: "text-faint hover:text-muted", |
| 43 | false: "text-muted hover:text-fg aria-[current=page]:font-medium aria-[current=page]:text-fg", |
| 44 | }, |
| 45 | }, |
| 46 | defaultVariants: { variant: "line", soon: false }, |
| 47 | }, |
| 48 | ); |
| 49 | |
| 50 | type Variant = NonNullable<VariantProps<typeof navTabsVariants>["variant"]>; |
| 51 | |
| 52 | /** A row of navigation tabs: `line` along a header's border, `pill` within a page. */ |
| 53 | export function NavTabs({ label, variant, className, children }: { label: string; variant?: Variant; className?: string; children: ReactNode }) { |
| 54 | return ( |
| 55 | <TabStrip label={label} className={cn(navTabsVariants({ variant }), className)}> |
| 56 | {children} |
| 57 | </TabStrip> |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * One tab: a link, current when `current` says so (not by its address, |
| 63 | * which would also mark a section's first page current under every other). |
| 64 | * `count` shows beside the label when there are any; `soon` marks a tab |
| 65 | * whose page is still coming. |
| 66 | */ |
| 67 | export function NavTab({ |
| 68 | current, |
| 69 | variant, |
| 70 | soon, |
| 71 | count, |
| 72 | className, |
| 73 | children, |
| 74 | ...props |
| 75 | }: ComponentProps<typeof Link> & VariantProps<typeof navTabVariants> & { current?: boolean; count?: number }) { |
| 76 | return ( |
| 77 | <Link |
| 78 | prefetch="intent" |
| 79 | aria-current={current ? "page" : undefined} |
| 80 | data-active={current || undefined} |
| 81 | className={cn(navTabVariants({ variant, soon }), className)} |
| 82 | {...props} |
| 83 | > |
| 84 | {children} |
| 85 | {count != null && count > 0 && <NavTabCount>{count}</NavTabCount>} |
| 86 | {soon && <SoonPill />} |
| 87 | </Link> |
| 88 | ); |
| 89 | } |
| 90 | |
| 91 | /** How many there are, beside a tab's label. */ |
| 92 | function NavTabCount({ children }: { children: ReactNode }) { |
| 93 | return <span className="rounded-full bg-raised px-1.5 text-xs leading-5 font-normal text-muted tabular-nums">{children}</span>; |
| 94 | } |