diff --git a/lib/experimental/Navigation/Tabs/index.stories.tsx b/lib/experimental/Navigation/Tabs/index.stories.tsx new file mode 100644 index 000000000..2d552d66c --- /dev/null +++ b/lib/experimental/Navigation/Tabs/index.stories.tsx @@ -0,0 +1,60 @@ +import { action } from "@storybook/addon-actions" +import type { Meta, StoryObj } from "@storybook/react" +import { useState } from "react" +import { Tabs } from "." + +const meta: Meta = { + component: Tabs, + tags: ["autodocs"], + argTypes: { + secondary: { + control: "boolean", + }, + }, +} + +export default meta +type Story = StoryObj + +const tabItems = [ + { label: "Overview", link: "/overview" }, + { label: "Courses", link: "/courses" }, + { label: "Categories", link: "/categories" }, + { label: "Catalog", link: "/catalog" }, + { label: "Requests", link: "/requests" }, +] + +const TabsExample = ({ secondary = false }: { secondary?: boolean }) => { + const [activeTab, setActiveTab] = useState("Overview") + + return ( +
e.preventDefault()}> + { + setActiveTab(tab.label) + action("Tab changed")(tab) + }} + secondary={secondary} + /> +

+ {activeTab} +

+
+ ) +} + +export const Primary: Story = { + args: { + secondary: false, + }, + render: (args) => , +} + +export const Secondary: Story = { + args: { + secondary: true, + }, + render: (args) => , +} diff --git a/lib/experimental/Navigation/Tabs/index.tsx b/lib/experimental/Navigation/Tabs/index.tsx new file mode 100644 index 000000000..dc453b362 --- /dev/null +++ b/lib/experimental/Navigation/Tabs/index.tsx @@ -0,0 +1,36 @@ +import { TabNavigation, TabNavigationLink } from "@/ui/tab-navigation" + +interface TabItem { + label: string + link: string +} + +interface TabsProps { + tabs: TabItem[] + activeTab: string + onTabChange: (tab: TabItem) => void + secondary?: boolean +} + +export function Tabs({ + tabs, + activeTab, + onTabChange, + secondary = false, +}: TabsProps) { + return ( + + {tabs.map((tab) => ( + onTabChange(tab)} + href={tab.link} + secondary={secondary} + > + {tab.label} + + ))} + + ) +} diff --git a/lib/ui/tab-navigation.tsx b/lib/ui/tab-navigation.tsx new file mode 100644 index 000000000..9f0630250 --- /dev/null +++ b/lib/ui/tab-navigation.tsx @@ -0,0 +1,137 @@ +import { cn } from "@/lib/utils" +import * as NavigationMenuPrimitives from "@radix-ui/react-navigation-menu" +import { cva, type VariantProps } from "class-variance-authority" +import { motion } from "framer-motion" +import * as React from "react" + +function getSubtree( + options: { asChild: boolean | undefined; children: React.ReactNode }, + content: React.ReactNode | ((children: React.ReactNode) => React.ReactNode) +) { + const { asChild, children } = options + if (!asChild) + return typeof content === "function" ? content(children) : content + + const firstChild = React.Children.only(children) as React.ReactElement + return React.cloneElement(firstChild, { + children: + typeof content === "function" + ? content(firstChild.props.children) + : content, + }) +} + +const tabNavigationVariants = cva( + "flex items-center justify-start gap-1 overflow-x-auto whitespace-nowrap border-x-0 border-b border-t-0 border-solid border-f1-border-secondary px-6 py-3 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", + { + variants: { + secondary: { + true: "bg-f1-background-secondary/25", + false: "bg-f1-background-transparent", + }, + }, + defaultVariants: { + secondary: false, + }, + } +) + +interface TabNavigationProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const TabNavigation = React.forwardRef< + React.ElementRef, + TabNavigationProps +>(({ className, children, secondary, ...props }, forwardedRef) => ( + + + {children} + + +)) + +TabNavigation.displayName = "TabNavigation" + +const tabNavigationLinkVariants = cva( + "flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 font-medium transition-all", + { + variants: { + secondary: { + true: "bg-f1-background/60 group-hover:border-f1-border group-data-[active=true]:border-f1-border group-data-[active=true]:text-f1-foreground", + false: + "bg-f1-background-transparent group-hover:bg-f1-background-secondary group-hover:text-f1-foreground group-data-[active=true]:bg-f1-background-secondary group-data-[active=true]:text-f1-foreground", + }, + disabled: { + true: "pointer-events-none text-f1-foreground-disabled", + }, + }, + defaultVariants: { + secondary: false, + disabled: false, + }, + } +) + +interface TabNavigationLinkProps + extends Omit< + React.ComponentPropsWithoutRef, + "onSelect" + >, + VariantProps { + active?: boolean +} + +const TabNavigationLink = React.forwardRef< + React.ElementRef, + TabNavigationLinkProps +>( + ( + { asChild, disabled, active, className, children, secondary, ...props }, + forwardedRef + ) => ( + + {}} + asChild={asChild} + {...props} + > + {getSubtree({ asChild, children }, (children) => ( + + {children} + {active && !secondary && ( + + )} + + ))} + + + ) +) + +TabNavigationLink.displayName = "TabNavigationLink" + +export { TabNavigation, TabNavigationLink } diff --git a/package-lock.json b/package-lock.json index 8f146ab71..4f63a5a55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "@factorialco/factorial-one", "version": "0.0.1", + "dependencies": { + "@radix-ui/react-navigation-menu": "^1.2.0" + }, "devDependencies": { "@chromatic-com/storybook": "^2.0.2", "@microsoft/api-extractor": "^7.47.9", @@ -2597,8 +2600,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@radix-ui/react-accordion": { "version": "1.2.0", @@ -2750,7 +2752,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -2777,7 +2778,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2793,7 +2793,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2846,7 +2845,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2862,7 +2860,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -2962,7 +2959,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -3041,6 +3037,42 @@ } } }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz", + "integrity": "sha512-OQ8tcwAOR0DhPlSY3e4VMXeHiol7la4PPdJWhhwJiJA+NLX0SaCaonOkRnI3gCDHoZ7Fo7bb/G6q25fRM2Y+3Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", @@ -3142,7 +3174,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -3167,7 +3198,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -3324,7 +3354,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -3465,7 +3494,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3481,7 +3509,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -3500,7 +3527,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -3519,7 +3545,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3535,7 +3560,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -3589,7 +3613,6 @@ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", "license": "MIT", - "peer": true, "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, diff --git a/package.json b/package.json index a0a40fa5b..0832a2633 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.0.7", + "@radix-ui/react-navigation-menu": "^1.2.0", "@tailwindcss/container-queries": "^0.1.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", @@ -123,4 +124,4 @@ "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "^4.22" } -} \ No newline at end of file +}