feat(docs): hero improved, tabs component initial structure

This commit is contained in:
Junior Garcia 2023-04-27 00:37:51 -03:00
parent d5ec85063b
commit b1bb36f898
27 changed files with 921 additions and 49 deletions

View File

@ -1,11 +1,16 @@
import {useState} from "react";
import {useState, FC} from "react";
import {Card, CardHeader, Button, Avatar, CardBody, CardFooter} from "@nextui-org/react";
import {clsx} from "@nextui-org/shared-utils";
export const UserTwitterCard = () => {
interface UserTwitterCardProps {
className?: string;
}
export const UserTwitterCard: FC<UserTwitterCardProps> = ({className}) => {
const [isFollowed, setIsFollowed] = useState(false);
return (
<Card className="max-w-[300px]">
<Card className={clsx("max-w-[300px]", className)}>
<CardHeader className="justify-between">
<div className="flex gap-5">
<Avatar isBordered radius="full" size="md" src="/avatars/avatar-1.png" />

View File

@ -58,7 +58,7 @@ export const DocsNavbar = () => {
<Navbar maxWidth="xl" position="sticky" onMenuOpenChange={setIsMenuOpen}>
<NavbarContent className="basis-1/5 sm:basis-full" justify="start">
<NavbarBrand className="gap-3">
<NextUILogo />
<NextUILogo auto />
<Dropdown placement="bottom-start">
<DropdownTrigger>
<Button

View File

@ -3,9 +3,9 @@ import {
Button,
Input,
Tooltip,
Card,
Image,
CardFooter,
Card,
CardBody,
Switch,
Spinner,
@ -15,6 +15,7 @@ import {ArrowRightIcon, MoonFilledIcon, SunFilledIcon} from "@nextui-org/shared-
import {useTheme} from "next-themes";
import dynamic from "next/dynamic";
import {NextUILogo} from "@/components";
import {GithubIcon} from "@/components/icons";
import {UserTwitterCard} from "@/components/demos";
@ -38,19 +39,9 @@ const FloatingComponents: React.FC = () => {
return (
<div className="flex flex-col gap-4">
<Input
className="w-[200px]"
color="secondary"
defaultValue="NextUI"
label="Input"
labelPosition="outside"
radius="xl"
variant="bordered"
onClear={() => {}}
/>
<Switch
classNames={{
base: "relative top-[140px] left-[240px] animate-[levitate_13s_ease_infinite_1s_reverse]",
startIcon: "text-white",
}}
endIcon={<MoonFilledIcon />}
@ -60,56 +51,112 @@ const FloatingComponents: React.FC = () => {
onChange={onChange}
/>
<UserTwitterCard />
<Card
isFooterBlurred
className="relative h-[120px] animate-[levitate_12s_ease_infinite_1s] left-[80px] top-[40px] z-0 max-w-fit"
radius="2xl"
>
<Image
className="object-cover -translate-y-6 h-[120%]"
src="/images/card-example-6.jpeg"
width={120}
/>
<CardFooter className="before:bg-black/10 before:border before:border-white/20 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
<p className="text-xs font-semibold text-white/80">Camera</p>
<p className="text-xs font-semibold text-white/80">$525</p>
</CardFooter>
</Card>
<Input
className="relative animate-[levitate_10s_ease_infinite] -left-[144px] top-[0px] w-[200px]"
color="secondary"
defaultValue="NextUI"
label="Input"
labelPosition="outside"
radius="xl"
variant="bordered"
onClear={() => {}}
/>
<UserTwitterCard className="animate-[levitate_16s_ease_infinite] relative border-none -left-[240px] top-[20px]" />
<Card className="animate-[levitate_18s_ease_infinite] relative left-[90px] -top-[140px] right-8 shadow-lg z-10 max-w-fit border-none">
<CardBody>
<NextUILogo small size={60} />
</CardBody>
</Card>
<div className="relative z-10 -top-[230px] -right-[200px] animate-[levitate_14s_ease_infinite_1s]">
<Pagination
isCompact
showControls
showShadow
classNames={{
base: "shadow-lg rounded-xl",
item: "bg-background dark:bg-content1",
prev: "bg-background dark:bg-content1",
next: "bg-background dark:bg-content1",
}}
initialPage={6}
total={10}
/>
</div>
<Tooltip
isOpen
showArrow
className="text-sm"
className="text-sm animate-[levitate_14s_ease_infinite]"
color="secondary"
content="Developers love Next.js"
placement="top"
>
<Button className="max-w-fit" color="secondary" size="sm" variant="flat">
<Button
className="relative -left-[120px] -top-[80px] max-w-fit animate-[levitate_14s_ease_infinite_0.5s]"
color="secondary"
size="sm"
variant="flat"
>
Tooltip
</Button>
</Tooltip>
<Card isFooterBlurred className="max-w-fit" radius="2xl">
<Image className="object-cover" height={400} src="/images/hero-card.png" />
<CardFooter className="bg-white/10 justify-between py-2 rounded-none absolute -bottom-0.5 z-10">
<Card className="relative animate-[levitate_16s_ease_infinite] left-[220px] -top-[260px] right-8 shadow-lg z-10 max-w-fit border-none">
<CardBody>
<Spinner color="secondary" size="xl" />
</CardBody>
</Card>
<Card
isFooterBlurred
className="relative animate-[levitate_12s_ease_infinite_1s] left-10 -top-[300px] z-0 max-w-fit"
radius="2xl"
>
<Image className="object-cover" height={200} src="/images/hero-card.jpeg" width={200} />
<CardFooter className="before:bg-white/10 overflow-hidden justify-between py-2 absolute before:rounded-xl rounded-xl bottom-1 w-[calc(100%_-_8px)] shadow-lg ml-1 z-10">
<p className="text-xs text-white/80">Available soon.</p>
<Button color="secondary" radius="full" size="xs" variant="flat">
Notify me
</Button>
</CardFooter>
</Card>
<Card className="max-w-fit border-none">
<CardBody>
<Spinner color="secondary" size="xl" />
</CardBody>
</Card>
<Pagination isCompact showControls showShadow initialPage={6} total={10} />
</div>
);
};
export const Hero = () => {
return (
<section className="flex relative w-full flex-nowrap justify-between items-center h-[calc(84vh_-_64px)]">
<div className="flex flex-col gap-6 w-2/3">
<section className="flex relative w-full flex-nowrap justify-between items-center h-[calc(90vh_-_64px)] max-h-[800px]">
<div className="flex flex-col gap-6 w-1/2">
<div>
<h1 className="text-6xl tracking-tight inline font-semibold">Make&nbsp;</h1>
<h1 className="text-6xl tracking-tight inline font-semibold bg-clip-text text-transparent bg-gradient-to-b from-[#FF1CF7] to-[#b249f8]">
<h1 className="text-5xl tracking-tight inline font-semibold">Make&nbsp;</h1>
<h1 className="text-5xl tracking-tight inline font-semibold bg-clip-text text-transparent bg-gradient-to-b from-[#FF1CF7] to-[#b249f8]">
beautiful&nbsp;
</h1>
<h1 className="text-6xl tracking-tight inline font-semibold">
<h1 className="text-5xl tracking-tight inline font-semibold">
websites regardless of your design experience.
</h1>
</div>
<h4 className="font-normal text-xl text-neutral-500">
<h4 className="text-xl font-light text-neutral-500">
Beautiful, fast and modern React UI library.
</h4>
<div className="flex items-center gap-4">
@ -131,10 +178,10 @@ export const Hero = () => {
</Button>
</div>
</div>
<div>
<div className="relative h-full">
<FloatingComponents />
</div>
<DynamicLopperBG className="absolute top-0 -z-50" />
<DynamicLopperBG className="absolute -top-1/2 -z-50" />
</section>
);
};

View File

@ -11,14 +11,7 @@ export interface LogoProps extends IconSvgProps {
className?: string;
}
export const NextUILogo: React.FC<LogoProps> = ({
auto = true,
size,
width,
height,
small,
...props
}) => {
export const NextUILogo: React.FC<LogoProps> = ({auto, size, width, height, small, ...props}) => {
const Small = () => (
<svg
className="data-[auto=true]:sm:hidden block text-foreground"

View File

@ -13,6 +13,7 @@
"@nextui-org/react": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/shared-icons": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"next": "13.3.0",
"next-themes": "^0.2.1",
"react": "^18.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -28,10 +28,28 @@ module.exports = {
'0%': { transform: 'scale(1)' },
'50%': { transform: 'scale(1.2)' },
'100%': { transform: 'scale(1)' },
},
levitate: {
"0%": {
transform: "translateY(0)",
},
"30%": {
transform: "translateY(-10px)",
},
"50%": {
transform: "translateY(4px)",
},
"70%": {
transform: "translateY(-15px)",
},
"100%": {
transform: "translateY(0)",
},
}
},
animation: {
'heartbeat': 'heartbeat 1s ease-in-out infinite',
'levitate': 'levitate 5s ease infinite',
}
},
plugins: [nextui()],

View File

@ -0,0 +1,24 @@
# @nextui-org/tabs
A Quick description of the component
> This is an internal utility, not intended for public usage.
## Installation
```sh
yarn add @nextui-org/tabs
# or
npm i @nextui-org/tabs
```
## Contribution
Yes please! See the
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
for details.
## Licence
This project is licensed under the terms of the
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).

View File

@ -0,0 +1,19 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {Tabs} from "../src";
describe("Tabs", () => {
it("should render correctly", () => {
const wrapper = render(<Tabs />);
expect(() => wrapper.unmount()).not.toThrow();
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
render(<Tabs ref={ref} />);
expect(ref.current).not.toBeNull();
});
});

View File

@ -0,0 +1,70 @@
{
"name": "@nextui-org/tabs",
"version": "2.0.0-beta.1",
"description": "Tabs organize content into multiple sections and allow users to navigate between them.",
"keywords": [
"tabs"
],
"author": "Junior Garcia <jrgarciadev@gmail.com>",
"homepage": "https://nextui.org",
"license": "MIT",
"main": "src/index.ts",
"sideEffects": false,
"files": [
"dist"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nextui-org/nextui.git",
"directory": "packages/components/tabs"
},
"bugs": {
"url": "https://github.com/nextui-org/nextui/issues"
},
"scripts": {
"build": "tsup src --dts",
"build:fast": "tsup src",
"dev": "yarn build:fast -- --watch",
"clean": "rimraf dist .turbo",
"typecheck": "tsc --noEmit",
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"peerDependencies": {
"react": ">=18",
"framer-motion": ">=6.2.8"
},
"dependencies": {
"@nextui-org/dom-utils": "workspace:*",
"@nextui-org/shared-utils": "workspace:*",
"@nextui-org/system": "workspace:*",
"@nextui-org/theme": "workspace:*",
"@nextui-org/aria-utils": "workspace:*",
"@nextui-org/framer-transitions": "workspace:*",
"@react-aria/focus": "^3.12.0",
"@react-aria/interactions": "^3.15.0",
"@react-aria/tabs": "^3.5.0",
"@react-aria/utils": "^3.16.0",
"@react-stately/tabs": "^3.4.0"
},
"devDependencies": {
"framer-motion": "^10.11.2",
"@react-types/shared": "^3.18.0",
"react-lorem-component": "0.13.0",
"@react-types/tabs": "^3.2.1",
"clean-package": "2.2.0",
"react": "^18.0.0"
},
"clean-package": "../../../clean-package.config.json",
"tsup": {
"clean": true,
"target": "es2019",
"format": [
"cjs",
"esm"
]
}
}

View File

@ -0,0 +1,15 @@
import {BaseItem, ItemProps} from "@nextui-org/aria-utils";
import {ReactNode} from "react";
interface Props<T extends object = {}> extends Omit<ItemProps<"div", T>, "children"> {
/**
* The content of the component.
*/
children?: ReactNode | null;
}
export type TabItemProps<T extends object = {}> = Props<T>;
const TabItemBase = BaseItem as <T extends object>(props: TabItemProps<T>) => JSX.Element;
export default TabItemBase;

View File

@ -0,0 +1,12 @@
import Tabs from "./tabs";
// export types
export type {TabsProps} from "./tabs";
export type {TabItemProps} from "./base/tab-item-base";
// export hooks
export {useTabs} from "./use-tabs";
// export components
export {Tabs};
export {default as TabItem} from "./base/tab-item-base";

View File

@ -0,0 +1,91 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
import {chain, filterDOMProps, mergeProps} from "@react-aria/utils";
import {useFocusRing} from "@react-aria/focus";
import {Node} from "@react-types/shared";
import {useTab} from "@react-aria/tabs";
import {useHover} from "@react-aria/interactions";
import {motion} from "framer-motion";
import {TRANSITION_EASINGS} from "@nextui-org/framer-transitions";
import {useTabsContext} from "./tabs-context";
export interface TabItemProps<T = object> extends HTMLNextUIProps<"div"> {
/**
* The tab item.
*/
item: Node<T>;
}
/**
* @internal
*/
const TabItem = forwardRef<TabItemProps, "div">((props, ref) => {
const {as, className, item, style, onClick, ...otherProps} = props;
const {key} = item;
const Component = as || "div";
const domRef = useDOMRef(ref);
const {slots, state, tabPanelId, classNames} = useTabsContext();
const {tabProps, isSelected, isDisabled, isPressed} = useTab({key}, state, domRef);
const {focusProps, isFocused, isFocusVisible} = useFocusRing();
const {hoverProps, isHovered} = useHover({
isDisabled,
});
const tabStyles = clsx(classNames?.tab, className, item.props?.className);
return (
<Component
ref={domRef}
data-focus={dataAttr(isFocused)}
data-focus-visible={dataAttr(isFocusVisible)}
data-hover={dataAttr(isHovered)}
data-hover-unselected={dataAttr(isHovered && !isSelected)}
data-pressed={dataAttr(isPressed)}
data-selected={dataAttr(isSelected)}
{...mergeProps(
tabProps,
focusProps,
hoverProps,
filterDOMProps(item.props, {labelable: true}),
otherProps,
)}
aria-controls={tabPanelId}
className={slots.tab?.({class: tabStyles})}
id={`${tabPanelId}-${key}`}
style={{
...style,
WebkitTapHighlightColor: "transparent",
}}
onClick={chain(onClick, tabProps.onClick)}
>
{isSelected && (
<motion.span
className={slots.cursor({class: classNames?.cursor})}
layoutDependency={false}
layoutId="tab-item-cursor"
transition={{
ease: TRANSITION_EASINGS.softSpring,
duration: 0.6,
}}
/>
)}
<div
className={slots.tabContent({
class: classNames?.tabContent,
})}
>
{item.rendered}
</div>
</Component>
);
});
TabItem.displayName = "NextUI.TabItem";
export default TabItem;

View File

@ -0,0 +1,58 @@
import type {AriaTabPanelProps} from "@react-aria/tabs";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
import {useTabPanel} from "@react-aria/tabs";
import {useFocusRing} from "@react-aria/focus";
import {useTabsContext} from "./tabs-context";
interface Props extends HTMLNextUIProps<"div"> {}
export type TabPanelProps = Props & AriaTabPanelProps;
/**
* @internal
*/
const TabPanel = forwardRef<TabPanelProps, "div">((props, ref) => {
const {as, className, ...otherProps} = props;
const Component = as || "div";
const domRef = useDOMRef(ref);
const {slots, tabPanelId, state, classNames} = useTabsContext();
const {tabPanelProps} = useTabPanel(props, state, domRef);
const {focusProps, isFocused, isFocusVisible} = useFocusRing();
const selectedItem = state.selectedItem;
const content = selectedItem?.props.children;
const tabPanelStyles = clsx(classNames?.panel, className, selectedItem.props?.className);
return (
<Component
ref={domRef}
data-focus={isFocused}
data-focus-visible={isFocusVisible}
{...mergeProps(
tabPanelProps,
focusProps,
filterDOMProps(selectedItem.props, {labelable: true}),
otherProps,
)}
aria-labelledby={`${tabPanelId}-${state.selectedItem?.key}`}
className={slots.panel?.({class: tabPanelStyles})}
id={tabPanelId}
>
{content}
</Component>
);
});
TabPanel.displayName = "NextUI.TabPanel";
export default TabPanel;

View File

@ -0,0 +1,10 @@
import {createContext} from "@nextui-org/shared-utils";
import {ContextType} from "./use-tabs";
export const [TabsProvider, useTabsContext] = createContext<ContextType>({
name: "TabsContext",
strict: true,
errorMessage:
"useTabsContext: `context` is undefined. Seems you forgot to wrap component within <Tabs />",
});

View File

@ -0,0 +1,32 @@
import {forwardRef} from "@nextui-org/system";
import {AriaTabProps} from "@react-aria/tabs";
import {TabsProvider} from "./tabs-context";
import {UseTabsProps, useTabs} from "./use-tabs";
import TabItem from "./tab-item";
import TabPanel from "./tab-panel";
interface Props extends Omit<UseTabsProps, "ref"> {}
export type TabsProps = Props & AriaTabProps;
const Tabs = forwardRef<TabsProps, "div">((props, ref) => {
const {Component, state, context, getBaseProps, getTabListProps} = useTabs({ref, ...props});
return (
<TabsProvider value={context}>
<div {...getBaseProps()}>
<Component {...getTabListProps()}>
{[...state.collection].map((item) => (
<TabItem key={item.key} item={item} {...item.props} />
))}
</Component>
</div>
<TabPanel key={state.selectedItem?.key} />
</TabsProvider>
);
});
Tabs.displayName = "NextUI.Tabs";
export default Tabs;

View File

@ -0,0 +1,100 @@
import type {TabsVariantProps, SlotsToClasses, TabsSlots, TabsReturnType} from "@nextui-org/theme";
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {tabs} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, ReactRef} from "@nextui-org/shared-utils";
import {useMemo, ReactNode, useId} from "react";
import {TabListState, TabListStateOptions, useTabListState} from "@react-stately/tabs";
import {AriaTabListOptions, useTabList} from "@react-aria/tabs";
import {filterDOMProps, mergeProps} from "@react-aria/utils";
export interface Props extends HTMLNextUIProps<"div"> {
/**
* Ref to the DOM node.
*/
ref?: ReactRef<HTMLElement | null>;
/*
* The list of `TabItem` elements.
*/
children?: ReactNode | ReactNode[];
/**
* Classname or List of classes to change the classNames of the element.
* if `className` is passed, it will be added to the base slot.
*
* @example
* ```ts
* <Tabs classNames={{
* base:"base-classes", // table wrapper
* tab: "tab-classes", // tab item
* panel: "panel-classes", // tab panel
* }} />
* ``
*/
classNames?: SlotsToClasses<TabsSlots>;
}
export type UseTabsProps<T = object> = Props &
TabsVariantProps &
TabListStateOptions<T> &
AriaTabListOptions<T>;
export type ContextType<T = object> = {
state: TabListState<T>;
slots: TabsReturnType;
tabPanelId: string;
classNames?: SlotsToClasses<TabsSlots>;
};
export function useTabs<T extends object>(originalProps: UseTabsProps<T>) {
const [props, variantProps] = mapPropsVariants(originalProps, tabs.variantKeys);
const {ref, as, className, classNames, ...otherProps} = props;
const Component = as || "div";
const domRef = useDOMRef(ref);
const state = useTabListState(otherProps);
const {tabListProps} = useTabList(otherProps, state, domRef);
const tabListId = useId();
const tabPanelId = useId();
const slots = useMemo(
() =>
tabs({
...variantProps,
className,
}),
[...Object.values(variantProps), className],
);
const tabListStyles = clsx(className, classNames?.tabList);
const context = useMemo<ContextType<T>>(
() => ({
state,
slots,
tabPanelId,
classNames,
}),
[state, slots, tabPanelId, classNames],
);
const getBaseProps: PropGetter = (props) => ({
className: slots.base({class: clsx(classNames?.base, props?.className)}),
...mergeProps(filterDOMProps(otherProps, {labelable: true}), props),
});
const getTabListProps: PropGetter = (props) => ({
ref: domRef,
className: slots.tabList({class: clsx(tabListStyles, props?.className)}),
...mergeProps(tabListProps, props),
id: tabListId,
});
return {Component, state, context, getBaseProps, getTabListProps};
}
export type UseTabsReturn = ReturnType<typeof useTabs>;

View File

@ -0,0 +1,65 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {tabs} from "@nextui-org/theme";
import Lorem from "react-lorem-component";
import {Tabs, TabItem, TabsProps} from "../src";
export default {
title: "Components/Tabs",
component: Tabs,
argTypes: {
color: {
control: {
type: "select",
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
},
},
radius: {
control: {
type: "select",
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
},
},
size: {
control: {
type: "select",
options: ["xs", "sm", "md", "lg", "xl"],
},
},
isDisabled: {
control: {
type: "boolean",
},
},
},
} as ComponentMeta<typeof Tabs>;
const defaultProps = {
...tabs.defaultVariants,
};
const Template: ComponentStory<typeof Tabs> = (args: TabsProps) => (
<Tabs aria-label="Tabs example" {...args}>
<TabItem key="world" title="World">
<Lorem count={1} sentenceUpperBound={20} />
</TabItem>
<TabItem key="ny" title="N.Y">
<Lorem count={1} sentenceUpperBound={30} />
</TabItem>
<TabItem key="business" title="Business">
<Lorem count={1} sentenceUpperBound={10} />
</TabItem>
<TabItem key="arts" title="Arts">
<Lorem count={1} sentenceUpperBound={50} />
</TabItem>
<TabItem key="science" title="Science">
<Lorem count={1} sentenceUpperBound={24} />
</TabItem>
</Tabs>
);
export const Default = Template.bind({});
Default.args = {
...defaultProps,
};

View File

@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
}
},
"include": ["src", "index.ts"]
}

View File

@ -68,7 +68,8 @@
"@nextui-org/table": "workspace:*",
"@nextui-org/spacer": "workspace:*",
"@nextui-org/divider": "workspace:*",
"@nextui-org/kbd": "workspace:*"
"@nextui-org/kbd": "workspace:*",
"@nextui-org/tabs": "workspace:*"
},
"peerDependencies": {
"react": ">=18",

View File

@ -28,3 +28,4 @@ export * from "@nextui-org/table";
export * from "@nextui-org/spacer";
export * from "@nextui-org/divider";
export * from "@nextui-org/kbd";
export * from "@nextui-org/tabs";

View File

@ -89,7 +89,6 @@ const button = tv({
isDisabled: {
true: "opacity-50 pointer-events-none",
},
isInGroup: {
true: "[&:not(:first-child):not(:last-child)]:rounded-none",
},

View File

@ -163,7 +163,17 @@ const card = tv({
},
isFooterBlurred: {
true: {
footer: "bg-background/10 backdrop-blur backdrop-saturate-150",
footer: [
"before:content-['']",
"before:block",
"before:z-[-1]",
"before:absolute",
"before:-top-px",
"before:inset-0",
"before:bg-background/10",
"before:backdrop-blur",
"before:backdrop-saturate-150",
],
},
},
isDisabled: {

View File

@ -34,3 +34,4 @@ export * from "./table";
export * from "./spacer";
export * from "./divider";
export * from "./kbd";
export * from "./tabs";

View File

@ -0,0 +1,177 @@
import type {VariantProps} from "tailwind-variants";
import {tv} from "tailwind-variants";
import {colorVariants} from "../utils";
/**
* Tabs wrapper **Tailwind Variants** component
*
* @example
* ```js
* const {base, tabList, tab, panel} = tabs({...})
*
* <div className={base())}>
* <div className={tabList())}>
* <div className={tab())}>Tab 1</div>
* <div className={tab())}>Tab 2</div>
* <div className={tab())}>Tab 3</div>
* </div>
* <div className={panel())}>Selected panel</div>
* </div>
* ```
*/
const tabs = tv({
slots: {
base: "inline-block",
tabList: ["flex", "items-center", "gap-2", "bg-neutral-100", "p-1"],
tab: [
"z-0",
"w-full",
"px-3",
"py-1",
"flex",
"group",
"relative",
"justify-center",
"items-center",
"outline-none",
"cursor-pointer",
"transition-opacity",
"text-neutral-600",
"data-[selected=true]:text-foreground",
"data-[hover-unselected=true]:opacity-70",
// focus ring
"data-[focus-visible=true]:outline-none",
"data-[focus-visible=true]:ring-2",
"data-[focus-visible=true]:ring-primary",
"data-[focus-visible=true]:ring-offset-2",
"data-[focus-visible=true]:ring-offset-background",
"data-[focus-visible=true]:dark:ring-offset-background-dark",
],
tabContent: ["relative", "z-10"],
cursor: ["absolute", "inset-0", "z-0", "bg-white", "shadow-md"],
panel: ["mt-3"],
},
variants: {
color: {
neutral: {
cursor: ["bg-background", "dark:bg-neutral"],
tabContent: "group-data-[selected=true]:text-neutral-foreground",
},
primary: {
cursor: colorVariants.solid.primary,
tabContent: "group-data-[selected=true]:text-primary-foreground",
},
secondary: {
cursor: colorVariants.solid.secondary,
tabContent: "group-data-[selected=true]:text-secondary-foreground",
},
success: {
cursor: colorVariants.solid.success,
tabContent: "group-data-[selected=true]:text-success-foreground",
},
warning: {
cursor: colorVariants.solid.warning,
tabContent: "group-data-[selected=true]:text-warning-foreground",
},
danger: {
cursor: colorVariants.solid.danger,
tabContent: "group-data-[selected=true]:text-danger-foreground",
},
},
size: {
xs: {
tabContent: "text-xs",
tab: "h-7",
},
sm: {
tabContent: "text-sm",
tab: "h-8",
},
md: {
tabContent: "text-sm",
tab: "h-9",
},
lg: {
tabContent: "text-base",
tab: "h-10",
},
xl: {
tabContent: "text-base",
tab: "h-11",
},
},
radius: {
none: {
tabList: "rounded-none",
tab: "rounded-none",
cursor: "rounded-none",
},
base: {
tabList: "rounded",
tab: "rounded",
cursor: "rounded",
},
sm: {
tabList: "rounded-sm",
tab: "rounded-sm",
cursor: "rounded-sm",
},
md: {
tabList: "rounded-md",
tab: "rounded-md",
cursor: "rounded-md",
},
lg: {
tabList: "rounded-lg",
tab: "rounded-lg",
cursor: "rounded-lg",
},
xl: {
tabList: "rounded-xl",
tab: "rounded-xl",
cursor: "rounded-xl",
},
"2xl": {
tabList: "rounded-2xl",
tab: "rounded-2xl",
cursor: "rounded-2xl",
},
"3xl": {
tabList: "rounded-3xl",
tab: "rounded-3xl",
cursor: "rounded-3xl",
},
full: {
tabList: "rounded-full",
tab: "rounded-full",
cursor: "rounded-full",
},
},
fullWidth: {
true: {
base: "w-full",
tabList: "w-full",
},
},
isDisabled: {
true: {
tabList: "opacity-50 pointer-events-none",
},
},
},
defaultVariants: {
radius: "lg",
color: "neutral",
size: "md",
fullWidth: false,
isDisabled: false,
},
});
export type TabsVariantProps = VariantProps<typeof tabs>;
export type TabsSlots = keyof ReturnType<typeof tabs>;
export type TabsReturnType = ReturnType<typeof tabs>;
export {tabs};

113
pnpm-lock.yaml generated
View File

@ -275,6 +275,9 @@ importers:
'@nextui-org/shared-icons':
specifier: workspace:*
version: link:../../packages/utilities/shared-icons
'@nextui-org/shared-utils':
specifier: workspace:*
version: link:../../packages/utilities/shared-utils
'@nextui-org/theme':
specifier: workspace:*
version: link:../../packages/core/theme
@ -1494,6 +1497,61 @@ importers:
specifier: ^18.2.0
version: 18.2.0
packages/components/tabs:
dependencies:
'@nextui-org/aria-utils':
specifier: workspace:*
version: link:../../utilities/aria-utils
'@nextui-org/dom-utils':
specifier: workspace:*
version: link:../../utilities/dom-utils
'@nextui-org/framer-transitions':
specifier: workspace:*
version: link:../../utilities/framer-transitions
'@nextui-org/shared-utils':
specifier: workspace:*
version: link:../../utilities/shared-utils
'@nextui-org/system':
specifier: workspace:*
version: link:../../core/system
'@nextui-org/theme':
specifier: workspace:*
version: link:../../core/theme
'@react-aria/focus':
specifier: ^3.12.0
version: 3.12.0(react@18.2.0)
'@react-aria/interactions':
specifier: ^3.15.0
version: 3.15.0(react@18.2.0)
'@react-aria/tabs':
specifier: ^3.5.0
version: 3.5.0(react@18.2.0)
'@react-aria/utils':
specifier: ^3.16.0
version: 3.16.0(react@18.2.0)
'@react-stately/tabs':
specifier: ^3.4.0
version: 3.4.0(react@18.2.0)
devDependencies:
'@react-types/shared':
specifier: ^3.18.0
version: 3.18.0(react@18.2.0)
'@react-types/tabs':
specifier: ^3.2.1
version: 3.2.1(react@18.2.0)
clean-package:
specifier: 2.2.0
version: 2.2.0
framer-motion:
specifier: ^10.11.2
version: 10.11.2(react-dom@18.2.0)(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
react-lorem-component:
specifier: 0.13.0
version: 0.13.0(react@18.2.0)
packages/components/tooltip:
dependencies:
'@nextui-org/aria-utils':
@ -1666,6 +1724,9 @@ importers:
'@nextui-org/table':
specifier: workspace:*
version: link:../../components/table
'@nextui-org/tabs':
specifier: workspace:*
version: link:../../components/tabs
'@nextui-org/theme':
specifier: workspace:*
version: link:../theme
@ -6252,6 +6313,24 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@react-aria/tabs@3.5.0(react@18.2.0):
resolution: {integrity: sha512-QnCoNHDmeRoPWLHsr1Q81RN/KymwU79XS/zHguhZ3fx59je9bswUDG77NjylcPRXoOEOZ18gZ+Y7reBVRhNEog==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/focus': 3.12.0(react@18.2.0)
'@react-aria/i18n': 3.7.1(react@18.2.0)
'@react-aria/interactions': 3.15.0(react@18.2.0)
'@react-aria/selection': 3.14.0(react@18.2.0)
'@react-aria/utils': 3.16.0(react@18.2.0)
'@react-stately/list': 3.8.0(react@18.2.0)
'@react-stately/tabs': 3.4.0(react@18.2.0)
'@react-types/shared': 3.18.0(react@18.2.0)
'@react-types/tabs': 3.2.1(react@18.2.0)
'@swc/helpers': 0.4.14
react: 18.2.0
dev: false
/@react-aria/toggle@3.6.0(react@18.2.0):
resolution: {integrity: sha512-W6xncx5zzqCaPU2XsgjWnACHL3WBpxphYLvF5XlICRg0nZVjGPIWPDDUGyDoPsSUeGMW2vxtFY6erKXtcy4Kgw==}
peerDependencies:
@ -6383,6 +6462,19 @@ packages:
react: 18.2.0
dev: false
/@react-stately/list@3.8.0(react@18.2.0):
resolution: {integrity: sha512-eJ1iUFnXPZi5MGW2h/RdNTrKtq4HLoAlFAQbC4eSPlET6VDeFsX9NkKhE/A111ia24DnWCqJB5zH20EvNbOxxA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-stately/collections': 3.7.0(react@18.2.0)
'@react-stately/selection': 3.13.0(react@18.2.0)
'@react-stately/utils': 3.6.0(react@18.2.0)
'@react-types/shared': 3.18.0(react@18.2.0)
'@swc/helpers': 0.4.14
react: 18.2.0
dev: false
/@react-stately/menu@3.5.1(react@18.2.0):
resolution: {integrity: sha512-nnuZlDBFIc3gB34kofbKDStFg9r8rijY+7ez2VWQmss72I9D7+JTn7OXJxV0oQt2lBYmNfS5W6bC9uXk3Z4dLg==}
peerDependencies:
@ -6446,6 +6538,19 @@ packages:
react: 18.2.0
dev: false
/@react-stately/tabs@3.4.0(react@18.2.0):
resolution: {integrity: sha512-GeU0cykAEsyTf2tWC7JZqqLrgxPT1WriCmu9QAswJ7Dev1PkPvwDy3CEhJ3QDklTlhiLXLZOooyHh37lZTjRdg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-stately/list': 3.8.0(react@18.2.0)
'@react-stately/utils': 3.6.0(react@18.2.0)
'@react-types/shared': 3.18.0(react@18.2.0)
'@react-types/tabs': 3.2.1(react@18.2.0)
'@swc/helpers': 0.4.14
react: 18.2.0
dev: false
/@react-stately/toggle@3.5.1(react@18.2.0):
resolution: {integrity: sha512-PF4ZaATpXWu7DkneGSZ2/PA6LJ1MrhKNiaENTZlbojXMRr5kK33wPzaDW7I8O25IUm0+rvQicv7A6QkEOxgOPg==}
peerDependencies:
@ -6631,6 +6736,14 @@ packages:
'@react-types/shared': 3.18.0(react@18.2.0)
react: 18.2.0
/@react-types/tabs@3.2.1(react@18.2.0):
resolution: {integrity: sha512-KgvhrYvISQUq540iuNc3bRvOCfLvaeqpB5VwDYR8amG1FVWHklCW8xx8Uz63SVkOvNtExYCrlw63M/OnjRUzOw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-types/shared': 3.18.0(react@18.2.0)
react: 18.2.0
/@react-types/textfield@3.7.1(react@18.2.0):
resolution: {integrity: sha512-6V5+6/VgDbmgN61pyVct1VrXb2hqq7Y43BFQ+/ZhFDlVaMpC5xKWKgW/gPbGLLc27gax8t2Brt7VHJj+d+yrUw==}
peerDependencies: