diff --git a/apps/www/content/primitives/components/tabs.mdx b/apps/www/content/primitives/components/tabs.mdx index 43c20e6f6..17a7455d6 100644 --- a/apps/www/content/primitives/components/tabs.mdx +++ b/apps/www/content/primitives/components/tabs.mdx @@ -1,6 +1,6 @@ --- title: Tabs -description: A set of layered sections of content—known as tab panels—that are displayed one at a time. +description: A set of layered sections of content that display one panel at a time. radix: link: https://www.radix-ui.com/docs/primitives/components/tabs api: https://www.radix-ui.com/docs/primitives/components/tabs#api-reference @@ -9,89 +9,156 @@ radix: ## Preview - - - - General - Hosting - Editor - Billing - SEO - - - - - - - General - Hosting - Editor - Billing - SEO - - - - - General - Hosting - Editor - Billing - SEO - - + + + + }>Hoisting + Hosting + }>Editor + Billing + SEO + + + General settings content + + + Hosting configuration content + + + Editor preferences content + + + Billing information content + + + SEO settings content + + +## Usage + +The Tabs component provides a way to organize content into multiple sections, displaying one section at a time. + ## Installation Install the component from your command line. - + -## Anatomy - -Import all parts and piece them together. - - + - General - Hosting - Editor - Billing - SEO + Tab One + Tab Two - - - - - General - Hosting - Editor - Billing - SEO + Tab one content + Tab two content +`} border/> + + +## Tabs Props + +The Tabs component is composed of several parts, each with their own props: + +### Root Props + +- `defaultValue`: The initial active tab value. If not passed, no tab will be selected by default. +- `value`: Controlled active tab value. +- `onValueChange`: Callback when active tab changes. +- `className`: Additional CSS class names. + +### List Props + +- `className`: Additional CSS class names + +### Trigger Props + +- `value`: Unique identifier for the tab (required) +- `icon`: Optional icon element to display +- `disabled`: Whether the tab is disabled +- `className`: Additional CSS class names + +### Content Props + +- `value`: Matching identifier for the tab (required) +- `className`: Additional CSS class names + +## Examples + +### Basic Tabs + + + + Account + Password + Settings - - - - - General - Hosting - Editor - Billing - SEO + Account settings + Password settings + Other settings +`, + }, + ]} +/> + +### With Icons + + + + + Home + } /> + + Home + Info + + `, + }, + ]} +/> + +### With Disabled Tab + + + + Active + Disabled - -`} border /> - + Active tab content + Disabled tab content +`, + }, + ]} +/> + +## Accessibility + +Tabs follow the [WAI-ARIA Tabs Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/). They include the following accessibility features: + +- Keyboard navigation between tabs using arrow keys +- Proper ARIA roles, states, and properties +- Focus management for tab panels diff --git a/apps/www/examples/shield-ts/assets.tsx b/apps/www/examples/shield-ts/assets.tsx index 0eb653102..62db62dd3 100644 --- a/apps/www/examples/shield-ts/assets.tsx +++ b/apps/www/examples/shield-ts/assets.tsx @@ -7,7 +7,7 @@ import { useTable } from "@raystack/apsara"; -import { toast, ToastContainer, Avatar, AvatarGroup, Button, Spinner, DropdownMenu, Breadcrumb, Chip, Flex, Text, Checkbox, InputField, Badge, Radio } from "@raystack/apsara/v1"; +import { toast, ToastContainer, Avatar, AvatarGroup, Button, Spinner, DropdownMenu, Breadcrumb, Chip, Flex, Text, Checkbox, InputField, Badge, Radio, Tabs } from "@raystack/apsara/v1"; import { getData, Payment } from "./data"; import { ApsaraColumnDef } from "@raystack/apsara/table/datatables.types"; @@ -207,7 +207,40 @@ const AssetsHeader = () => { justify="between" style={{ width: "100%", padding: "4px", paddingTop: "48px" }} > - + + + + }> + Home + + + Hosting + + } disabled /> + + Billing + + + SEO + + + + General settings content + + + Hosting configuration content + + + Editor preferences content + + + Billing information content + + + SEO settings content + + + {/* Assets */} {/*
diff --git a/apps/www/utils/routes.ts b/apps/www/utils/routes.ts index 7abaafde4..ec81d801b 100644 --- a/apps/www/utils/routes.ts +++ b/apps/www/utils/routes.ts @@ -92,7 +92,7 @@ export const primitivesRoutes = [ }, { title: "Switch", slug: "docs/primitives/components/switch", newBadge: true }, { title: "Slider", slug: "docs/primitives/components/slider", newBadge: true }, - { title: "Tabs", slug: "docs/primitives/components/tabs" }, + { title: "Tabs", slug: "docs/primitives/components/tabs", newBadge: true }, { title: "Table", slug: "docs/primitives/components/table" }, { title: "Text", diff --git a/packages/raystack/tabs/tabs.tsx b/packages/raystack/tabs/tabs.tsx index 1d9492b4c..107be1c9f 100644 --- a/packages/raystack/tabs/tabs.tsx +++ b/packages/raystack/tabs/tabs.tsx @@ -4,16 +4,25 @@ import React, { ComponentPropsWithoutRef } from "react"; import styles from "./tabs.module.css"; const root = cva(styles.root); + +/** + * @deprecated Use Tabs from '@raystack/apsara/v1' instead. + */ export interface TabsRootProps extends ComponentPropsWithoutRef, VariantProps {} + +/** + * @deprecated Use TabsRoot from '@raystack/apsara/v1' instead. + */ const TabsRoot = ({ className, ...props }: TabsRootProps) => ( ); TabsRoot.displayName = TabsPrimitive.Root.displayName; + const tablist = cva(styles.tablist, { variants: { underline: { @@ -24,10 +33,18 @@ const tablist = cva(styles.tablist, { }, }, }); + +/** + * @deprecated Use TabsListProps from '@raystack/apsara/v1' instead. + */ export interface TabsListProps extends ComponentPropsWithoutRef, VariantProps {} + +/** + * @deprecated Use TabsList from '@raystack/apsara/v1' instead. + */ const TabsList = React.forwardRef< React.ElementRef, TabsListProps @@ -40,28 +57,48 @@ const TabsList = React.forwardRef< )); TabsList.displayName = TabsPrimitive.List.displayName; + const content = cva(styles.content); + +/** + * @deprecated Use ContentProps from '@raystack/apsara/v1' instead. + */ export interface ContentProps extends ComponentPropsWithoutRef, VariantProps {} + +/** + * @deprecated Use Tabs from '@raystack/apsara/v1' instead. + */ const TabsContent = ({ className, ...props }: ContentProps) => ( ); TabsContent.displayName = TabsPrimitive.Content.displayName; + const trigger = cva(styles.trigger); + +/** + * @deprecated Use Tabs from '@raystack/apsara/v1' instead. + */ export interface TabsTriggerProps extends ComponentPropsWithoutRef, VariantProps {} +/** + * @deprecated Use Tabs from '@raystack/apsara/v1' instead. + */ const TabsTrigger = ({ className, ...props }: TabsTriggerProps) => ( ); TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; +/** + * @deprecated Use Tabs from '@raystack/apsara/v1' instead. + */ export const Tabs = Object.assign(TabsRoot, { Trigger: TabsTrigger, Content: TabsContent, diff --git a/packages/raystack/v1/components/tabs/index.tsx b/packages/raystack/v1/components/tabs/index.tsx new file mode 100644 index 000000000..c17b64023 --- /dev/null +++ b/packages/raystack/v1/components/tabs/index.tsx @@ -0,0 +1 @@ +export { Tabs } from "./tabs"; \ No newline at end of file diff --git a/packages/raystack/v1/components/tabs/tabs.module.css b/packages/raystack/v1/components/tabs/tabs.module.css new file mode 100644 index 000000000..8580cf9cb --- /dev/null +++ b/packages/raystack/v1/components/tabs/tabs.module.css @@ -0,0 +1,77 @@ +.root { + display: flex; + flex-direction: column; + width: 100%; +} + +.list { + display: flex; + align-items: center; + gap: var(--rs-space-2); + background-color: var(--rs-color-background-neutral-secondary); + padding: var(--rs-space-1); + border-radius: var(--rs-radius-3); + width: 100%; + box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.04) inset; +} + +.trigger { + all: unset; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--rs-space-2); + font-size: 12px; + font-weight: 500; + padding: var(--rs-space-2) var(--rs-space-3); + font-family: var(--rs-font-body); + color: var(--rs-color-text-base-secondary); + cursor: pointer; + border-radius: var(--rs-radius-2); + transition: all 0.2s ease; + flex: 1; + text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + height: 28px; + line-height: 1; + box-sizing: border-box; +} + +.trigger:hover:not([data-disabled]) { + color: var(--rs-color-text-base-primary); +} + +.trigger[data-state='active'] { + background-color: var(--rs-color-background-base-primary); + color: var(--rs-color-text-base-primary); + box-shadow: 0px 1px 1px 0px rgba(0, 0, 0, 0.06), 0px 4px 4px -1px rgba(0, 0, 0, 0.02); + font-size: 12px; + font-weight: 500; +} + +.trigger[data-disabled] { + opacity: 0.5; + cursor: not-allowed; + color: var(--rs-color-text-base-secondary); + pointer-events: none; +} + +.trigger-icon { + display: inline-flex; + width: 16px; + height: 16px; + color: currentColor; + justify-content: center; + align-items: center; + flex-shrink: 0; +} + +.content { + outline: none; +} + +.content[data-state='inactive'] { + display: none; +} \ No newline at end of file diff --git a/packages/raystack/v1/components/tabs/tabs.tsx b/packages/raystack/v1/components/tabs/tabs.tsx new file mode 100644 index 000000000..f15d9967f --- /dev/null +++ b/packages/raystack/v1/components/tabs/tabs.tsx @@ -0,0 +1,86 @@ +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { cva, type VariantProps } from "class-variance-authority"; +import { ComponentPropsWithoutRef, ElementRef, forwardRef, ReactNode } from "react"; + +import styles from "./tabs.module.css"; + +const root = cva(styles.root); +const list = cva(styles.list); +const trigger = cva(styles.trigger); +const content = cva(styles.content); + +interface TabsRootProps + extends ComponentPropsWithoutRef, + VariantProps { + defaultValue?: string; + 'aria-label'?: string; +} + +interface TabsTriggerProps + extends ComponentPropsWithoutRef { + icon?: ReactNode; + disabled?: boolean; +} + +const TabsRoot = forwardRef, TabsRootProps>( + ({ className, 'aria-label': ariaLabel, ...props }, ref) => ( + + ) +); + +const TabsList = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +const TabsTrigger = forwardRef< + ElementRef, + TabsTriggerProps +>(({ className, icon, children, disabled, ...props }, ref) => ( + + {icon && {icon}} + {children} + +)); + +const TabsContent = forwardRef< + ElementRef, + ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +TabsRoot.displayName = TabsPrimitive.Root.displayName; +TabsList.displayName = TabsPrimitive.List.displayName; +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export const Tabs = { + Root: TabsRoot, + List: TabsList, + Trigger: TabsTrigger, + Content: TabsContent, +}; \ No newline at end of file diff --git a/packages/raystack/v1/index.tsx b/packages/raystack/v1/index.tsx index 7f86435a5..833a4743c 100644 --- a/packages/raystack/v1/index.tsx +++ b/packages/raystack/v1/index.tsx @@ -22,6 +22,7 @@ export { Tooltip } from "./components/tooltip"; export { TextArea } from "./components/text-area"; export { Switch } from "./components/switch"; export { Slider } from "./components/slider"; +export { Tabs } from "./components/tabs"; export { ThemeProvider, ThemeProviderProps,