Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(components): add Tabs component #17

Merged
merged 1 commit into from
Jul 30, 2024
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
7 changes: 7 additions & 0 deletions apps/www/components/preview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Avatar } from "ruru-ui/components/avatar";
import { Checkbox } from "ruru-ui/components/checkbox";
import { Input } from "ruru-ui/components/input";
import { Textarea } from "ruru-ui/components/textarea";
import { Tab, Tabs } from "ruru-ui/components/tabs";
import {
Tooltip,
TooltipContent,
Expand All @@ -15,6 +16,7 @@ import {
} from "ruru-ui/components/tooltip";

import { BadgePreview } from "../badgePreview";
import Tabspreview from "../tabs";

export default {
button: (
Expand Down Expand Up @@ -112,4 +114,9 @@ export default {
</div>
</Wrapper>
),
tabs: (
<Wrapper>
<Tabspreview />
</Wrapper>
),
} as Record<string, ReactNode>;
48 changes: 48 additions & 0 deletions apps/www/components/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";
import { Tab, Tabs } from "ruru-ui/components/tabs";

const Tabspreview = () => {
return (
<div>
<div className="flex items-center justify-center gap-4">
<Tabs defaultIndex={2} items={["Javascript", "Rust", "Typescript"]}>
<Tab value="Javascript">
<div>
<h3>Javascript</h3>
<p>
JavaScript is a programming language that conforms to the
ECMAScript specification. JavaScript is high-level, often
just-in-time compiled, and multi-paradigm. It has curly-bracket
syntax, dynamic typing, prototype-based object-orientation, and
first-class functions.
</p>
</div>
</Tab>
<Tab value="Rust">
<div>
<h3>Rust</h3>
<p>
Rust is a multi-paradigm systems programming language focused on
safety, especially safe concurrency. Rust is syntactically
similar to C++, but can guarantee memory safety by using a
borrow checker to validate references.
</p>
</div>
</Tab>
<Tab value="Typescript">
<div>
<h3>Typescript</h3>
<p>
TypeScript is a programming language developed and maintained by
Microsoft. It is a strict syntactical superset of JavaScript and
adds optional static typing to the language.
</p>
</div>
</Tab>
</Tabs>
</div>
</div>
);
};

export default Tabspreview;
159 changes: 159 additions & 0 deletions apps/www/content/docs/components/tabs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: Tabs
description: The Tabs component is used to organize content into multiple sections with navigation.
preview: tabs
---

import { Tabs as Rutabs, Tab as Rutab } from "ruru-ui/components/tabs";
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

## Usage

<Tabs items={["Preview", "Code"]}>
<Tab className={"flex justify-center"} value="Preview" >
<div className="w-[80%]">
<Rutabs items={["Apple", "Orange", "Mango"]}>
<Rutab value={"Apple"}>Apple</Rutab>
<Rutab value={"Orange"}>Orange</Rutab>
<Rutab value={"Mango"}>Mango</Rutab>
</Rutabs>
</div>
</Tab>
<Tab className={"-mt-8"} value="Code">
```tsx
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

export function TabsDemo() {
return (
<div className="w-[80%]">
<Tabs items={["Apple", "Orange", "Mango"]}>
<Tab value={"Apple"}>Apple</Tab>
<Tab value={"Orange"}>Orange</Tab>
<Tab value={"Mango"}>Mango</Tab>
</Tabs>
</div>
);
}

```
</Tab>

</Tabs>

## Variants

### Default Tabs

<Tabs items={["Preview", "Code"]}>
<Tab className={"flex justify-center"} value="Preview" >
<div className="w-[80%]">
<Rutabs items={["Apple", "Orange", "Mango"]}>
<Rutab value={"Apple"}>Apple</Rutab>
<Rutab value={"Orange"}>Orange</Rutab>
<Rutab value={"Mango"}>Mango</Rutab>
</Rutabs>
</div>
</Tab>
<Tab className={"-mt-8"} value="Code">
```tsx
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

export function DefaultTabsDemo() {
return (
<div className="w-[80%]">
<Tabs items={["Apple", "Orange", "Mango"]}>
<Tab value={"Apple"}>Apple</Tab>
<Tab value={"Orange"}>Orange</Tab>
<Tab value={"Mango"}>Mango</Tab>
</Tabs>
</div>
);
}

```
</Tab>

</Tabs>

### Disabled Tabs

<Tabs items={["Preview", "Code"]}>
<Tab className={"flex justify-center"} value="Preview" >
<div className="w-[80%]">
<Rutabs disabled items={["Apple", "Orange", "Mango"]}>
<Rutab value={"Apple"}>Apple</Rutab>
<Rutab value={"Orange"}>Orange</Rutab>
<Rutab value={"Mango"}>Mango</Rutab>
</Rutabs>
</div>
</Tab>
<Tab className={"-mt-8"} value="Code">
```tsx
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

export function DisabledTabsDemo() {
return (
<div className="w-[80%]">
<Tabs disabled items={["Apple", "Orange", "Mango"]}>
<Tab value={"Apple"}>Apple</Tab>
<Tab value={"Orange"}>Orange</Tab>
<Tab value={"Mango"}>Mango</Tab>
</Tabs>
</div>
);
}
```
</Tab>

</Tabs>

### Default Index Tabs

<Tabs items={["Preview", "Code"]}>
<Tab className={"flex justify-center"} value="Preview" >
<div className="w-[80%]">
<Rutabs defaultIndex={2} items={["Apple", "Orange", "Mango"]}>
<Rutab value={"Apple"}>Apple</Rutab>
<Rutab value={"Orange"}>Orange</Rutab>
<Rutab value={"Mango"}>Mango</Rutab>
</Rutabs>
</div>
</Tab>
<Tab className={"-mt-8"} value="Code">
```tsx
import { Tabs, Tab } from "fumadocs-ui/components/tabs";

export function DefaultIndexTabsDemo() {
return (
<div className="w-[80%]">
<Tabs defaultIndex={2} items={["Apple", "Orange", "Mango"]}>
<Tab value={"Apple"}>Apple</Tab>
<Tab value={"Orange"}>Orange</Tab>
<Tab value={"Mango"}>Mango</Tab>
</Tabs>
</div>
);
}
```
</Tab>

</Tabs>

## Props

### Tabs

| Name | Type | Default | Description |
| ---------------- | -------- | ----------- | --------------------------------------- |
| **groupId** | string | `undefined` | Identifier for sharing value of tabs |
| **persist** | boolean | `false` | Enable persistent state |
| **defaultIndex** | number | `0` | Default index of the selected tab |
| **disabled** | boolean | `false` | Disable all tabs |
| **items** | string[] | `[]` | Array of items to be used as tab labels |

### Tab

| Name | Type | Default | Description |
| ------------- | ------ | ------- | ------------------------------------------------------- |
| **value** | string | | Value of the tab, used for identifying the selected tab |
| **className** | string | `""` | Additional class names for the container |
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-direction": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@swc/core": "^1.6.13",
"@tailwindcss/typography": "^0.5.13",
Expand Down
144 changes: 144 additions & 0 deletions packages/ui/src/components/tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"use client";

import type {
TabsContentProps,
TabsProps as BaseProps,
} from "@radix-ui/react-tabs";
import { useMemo, useState, useCallback, useLayoutEffect } from "react";
import { cn } from "@/utils/cn";

import * as Primitive from "@/components/ui/tabs";
export * as Primitive from "@/components/ui/tabs";

type ChangeListener = (v: string) => void;
const listeners = new Map<string, ChangeListener[]>();

function addChangeListener(id: string, listener: ChangeListener): void {
const list = listeners.get(id) ?? [];
list.push(listener);
listeners.set(id, list);
}

function removeChangeListener(id: string, listener: ChangeListener): void {
const list = listeners.get(id) ?? [];
listeners.set(
id,
list.filter((item) => item !== listener),
);
}

function update(id: string, v: string, persist: boolean): void {
listeners.get(id)?.forEach((item) => {
item(v);
});

if (persist) localStorage.setItem(id, v);
else sessionStorage.setItem(id, v);
}

export interface TabsProps extends BaseProps {
/**
* Identifier for Sharing value of tabs
* @default undefined
*/
groupId?: string;

/**
* Enable persistent
* @default false
*/
persist?: boolean;
/**
* @default 0
*/
defaultIndex?: number;
/**
* @default false
*/
disabled?: boolean;
/**
* Tabs items
* @default []
*/
items?: string[];
}

export function Tabs({
groupId,
items = [],
persist = false,
defaultIndex = 0,
disabled = false,
...props
}: TabsProps): React.ReactElement {
const values = useMemo(() => items.map((item) => toValue(item)), [items]);
const [value, setValue] = useState(values[defaultIndex]);

useLayoutEffect(() => {
if (!groupId) return;

const onUpdate: ChangeListener = (v) => {
if (values.includes(v)) setValue(v);
};

const previous = persist
? localStorage.getItem(groupId)
: sessionStorage.getItem(groupId);

if (previous) onUpdate(previous);
addChangeListener(groupId, onUpdate);
return () => {
removeChangeListener(groupId, onUpdate);
};
}, [groupId, persist, values]);

const onValueChange = useCallback(
(v: string) => {
if (groupId) {
update(groupId, v, persist);
} else {
setValue(v);
}
},
[groupId, persist],
);

return (
<Primitive.Tabs
value={value}
onValueChange={onValueChange}
{...props}
className={cn("my-4", props.className)}
>
<Primitive.TabsList>
{values.map((v, i) => (
<Primitive.TabsTrigger disabled={disabled} key={v} value={v}>
{items[i]}
</Primitive.TabsTrigger>
))}
</Primitive.TabsList>
{props.children}
</Primitive.Tabs>
);
}

function toValue(v: string): string {
return v.toLowerCase().replace(/\s/, "-");
}

export function Tab({
value,
className,
...props
}: TabsContentProps): React.ReactElement {
return (
<Primitive.TabsContent
value={toValue(value)}
className={cn(
"prose-no-margin [&>figure:only-child]:-m-4 [&>figure:only-child]:rounded-none [&>figure:only-child]:border-none",
className,
)}
{...props}
/>
);
}
Loading