From 6cb097b49a0e2f697f10bede39e498152db668b5 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 26 Mar 2026 16:27:10 +0900 Subject: [PATCH 1/7] feat: add Tabs compound component backed by Base UI --- .changeset/add-tabs-component.md | 18 +++ docs/components/tabs.md | 107 ++++++++++++ .../src__components__tabs.test.tsx.snap | 7 + packages/core/src/components/tabs.test.tsx | 152 ++++++++++++++++++ packages/core/src/components/tabs.tsx | 103 ++++++++++++ packages/core/src/index.ts | 1 + 6 files changed, 388 insertions(+) create mode 100644 .changeset/add-tabs-component.md create mode 100644 docs/components/tabs.md create mode 100644 packages/core/__snapshots__/src__components__tabs.test.tsx.snap create mode 100644 packages/core/src/components/tabs.test.tsx create mode 100644 packages/core/src/components/tabs.tsx diff --git a/.changeset/add-tabs-component.md b/.changeset/add-tabs-component.md new file mode 100644 index 00000000..7951d861 --- /dev/null +++ b/.changeset/add-tabs-component.md @@ -0,0 +1,18 @@ +--- +"@tailor-platform/app-shell": minor +--- + +Add `Tabs` compound component for tab-based navigation, backed by Base UI's Tabs primitive. + +```tsx +import { Tabs } from "@tailor-platform/app-shell"; + + + + Overview + Projects + + Overview content + Projects content +; +``` diff --git a/docs/components/tabs.md b/docs/components/tabs.md new file mode 100644 index 00000000..45cb545a --- /dev/null +++ b/docs/components/tabs.md @@ -0,0 +1,107 @@ +--- +title: Tabs +description: Tab navigation with a compound component API +--- + +# Tabs + +The `Tabs` component provides tab-based navigation for toggling between related panels on the same page. It is backed by Base UI's Tabs primitive. + +## Import + +```tsx +import { Tabs } from "@tailor-platform/app-shell"; +``` + +## Basic Usage + +```tsx + + + Overview + Projects + Account + + Overview content + Projects content + Account content + +``` + +## Sub-components + +| Sub-component | Description | +| ------------- | -------------------------------------------------------------- | +| `Tabs.Root` | Manages tab selection state | +| `Tabs.List` | Groups the individual tab buttons | +| `Tabs.Tab` | An interactive tab button that toggles the corresponding panel | +| `Tabs.Panel` | A panel displayed when the corresponding tab is active | + +## Props + +### Tabs.Root Props + +| Prop | Type | Default | Description | +| --------------- | ---------------------------- | -------------- | --------------------------------------- | +| `defaultValue` | `Tabs.Tab.Value` | `0` | Initial active tab value (uncontrolled) | +| `value` | `Tabs.Tab.Value` | - | Controlled active tab value | +| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes | +| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Orientation of the tabs | +| `className` | `string` | - | Additional CSS classes for root | +| `children` | `React.ReactNode` | - | Tabs sub-components | + +### Tabs.List Props + +Accepts `className` and all standard HTML `
` props. + +### Tabs.Tab Props + +| Prop | Type | Default | Description | +| ---------- | ---------------- | ------- | ---------------------------------- | +| `value` | `Tabs.Tab.Value` | - | **Required.** The value of the tab | +| `disabled` | `boolean` | - | Whether the tab is disabled | + +Also accepts `className` and all standard HTML `
Content 1
"`; + +exports[`Tabs > snapshots > tabs with disabled tab 1`] = `"
Content 1
"`; + +exports[`Tabs > snapshots > tabs with three tabs 1`] = `"
Overview content
"`; diff --git a/packages/core/src/components/tabs.test.tsx b/packages/core/src/components/tabs.test.tsx new file mode 100644 index 00000000..e7191048 --- /dev/null +++ b/packages/core/src/components/tabs.test.tsx @@ -0,0 +1,152 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Tabs } from "./tabs"; + +afterEach(() => { + cleanup(); +}); + +describe("Tabs", () => { + // ========================================================================== + // Snapshots — verify full DOM structure for tabs variations + // ========================================================================== + + describe("snapshots", () => { + it("default tabs", () => { + const { container } = render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + expect(container.innerHTML).toMatchSnapshot(); + }); + + it("tabs with three tabs", () => { + const { container } = render( + + + Overview + Projects + Account + + Overview content + Projects content + Account content + , + ); + expect(container.innerHTML).toMatchSnapshot(); + }); + + it("tabs with disabled tab", () => { + const { container } = render( + + + Tab 1 + + Tab 2 + + + Content 1 + Content 2 + , + ); + expect(container.innerHTML).toMatchSnapshot(); + }); + }); + + it("renders all tabs", () => { + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + + expect(screen.getByText("Tab 1")).toBeDefined(); + expect(screen.getByText("Tab 2")).toBeDefined(); + }); + + it("displays the active panel content", () => { + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + + expect(screen.getByText("Content 1")).toBeDefined(); + }); + + it("switches panel on tab click", async () => { + const user = userEvent.setup(); + + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + + await user.click(screen.getByText("Tab 2")); + + await waitFor(() => { + expect(screen.getByText("Content 2")).toBeDefined(); + }); + }); + + it("calls onValueChange when tab is clicked", async () => { + const handleChange = vi.fn(); + const user = userEvent.setup(); + + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + + await user.click(screen.getByText("Tab 2")); + + await waitFor(() => { + expect(handleChange).toHaveBeenCalled(); + expect(handleChange.mock.calls[0][0]).toBe("tab2"); + }); + }); + + it("supports controlled value", () => { + render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + + expect(screen.getByText("Content 2")).toBeDefined(); + }); +}); diff --git a/packages/core/src/components/tabs.tsx b/packages/core/src/components/tabs.tsx new file mode 100644 index 00000000..a328c008 --- /dev/null +++ b/packages/core/src/components/tabs.tsx @@ -0,0 +1,103 @@ +import * as React from "react"; +import { Tabs as BaseTabs } from "@base-ui/react/tabs"; + +import { cn } from "@/lib/utils"; + +// Only the props relevant to the Tabs abstraction are picked from BaseTabs.Root. +// Base UI-internal props are intentionally excluded so that upstream changes +// don't leak as breaking changes to consumers. +type RootProps = Pick< + React.ComponentProps, + "defaultValue" | "value" | "onValueChange" | "orientation" +> & { + children: React.ReactNode; + className?: string; +}; + +/** + * The root component that manages tab selection state. + * + * @example + * ```tsx + * + * + * Overview + * Projects + * Account + * + * Overview content + * Projects content + * Account content + * + * ``` + */ +function Root({ className, children, ...props }: RootProps) { + return ( + + {children} + + ); +} +Root.displayName = "Tabs.Root"; + +/** Groups the individual tab buttons. */ +function List({ className, children, ...props }: React.ComponentProps) { + return ( + + {children} + + ); +} +List.displayName = "Tabs.List"; + +/** An individual interactive tab button that toggles the corresponding panel. */ +function Tab({ className, children, ...props }: React.ComponentProps) { + return ( + + {children} + + ); +} +Tab.displayName = "Tabs.Tab"; + +/** A panel displayed when the corresponding tab is active. */ +function Panel({ className, children, ...props }: React.ComponentProps) { + return ( + + {children} + + ); +} +Panel.displayName = "Tabs.Panel"; + +export const Tabs = { + Root, + List, + Tab, + Panel, +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index daef45a1..2ac50c9e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -116,6 +116,7 @@ export { Fieldset } from "./components/fieldset"; export { Form, type FormProps } from "./components/form"; export { Menu } from "./components/menu"; export { Sheet } from "./components/sheet"; +export { Tabs } from "./components/tabs"; export { Tooltip } from "./components/tooltip"; export { Select, type SelectAsyncFetcher } from "./components/select-standalone"; export { Combobox, type ComboboxAsyncFetcher } from "./components/combobox-standalone"; From cd85bdf42226f5579c93bae4de466e6bc33dab57 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 25 May 2026 11:48:40 +0900 Subject: [PATCH 2/7] Update variants and tests --- examples/vite-app/src/pages/settings/page.tsx | 125 +++++++++++------- .../src__components__tabs.test.tsx.snap | 8 +- packages/core/src/components/tabs.test.tsx | 14 ++ packages/core/src/components/tabs.tsx | 28 +++- 4 files changed, 117 insertions(+), 58 deletions(-) diff --git a/examples/vite-app/src/pages/settings/page.tsx b/examples/vite-app/src/pages/settings/page.tsx index 5f112e41..6af6cad9 100644 --- a/examples/vite-app/src/pages/settings/page.tsx +++ b/examples/vite-app/src/pages/settings/page.tsx @@ -1,5 +1,13 @@ import { useState } from "react"; -import { Layout, Input, Button, Dialog, type AppShellPageProps } from "@tailor-platform/app-shell"; +import { + Layout, + Input, + Button, + Dialog, + Tabs, + type AppShellPageProps, + Card, +} from "@tailor-platform/app-shell"; import { labels } from "../../i18n-labels"; const SettingsIcon = () => ( @@ -27,56 +35,77 @@ const SettingsPage = () => { -

+

This page is at{" "} src/pages/settings/page.tsx

-
-
- - setName(e.target.value)} /> -
-
- - setEmail(e.target.value)} - /> -
-
- - }>Save Changes - - - Confirm Changes - - Save "{name}" as the application name and " - {email}" as the admin email? - - - - }>Cancel - }>Confirm - - - - -
-
+ + + + + General + Notifications + Account + + +
+
+ + setName(e.target.value)} /> +
+
+ + setEmail(e.target.value)} + /> +
+
+ + }>Save Changes + + + Confirm Changes + + Save "{name}" as the application name and " + {email}" as the admin email? + + + + }>Cancel + }>Confirm + + + + +
+
+
+ +

+ Configure your notification preferences here. +

+
+ +

Manage your account settings here.

+
+
+
+
); diff --git a/packages/core/__snapshots__/src__components__tabs.test.tsx.snap b/packages/core/__snapshots__/src__components__tabs.test.tsx.snap index 13dce3b4..8259ccb3 100644 --- a/packages/core/__snapshots__/src__components__tabs.test.tsx.snap +++ b/packages/core/__snapshots__/src__components__tabs.test.tsx.snap @@ -1,7 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Tabs > snapshots > default tabs 1`] = `"
Content 1
"`; +exports[`Tabs > snapshots > default tabs 1`] = `"
Content 1
"`; -exports[`Tabs > snapshots > tabs with disabled tab 1`] = `"
Content 1
"`; +exports[`Tabs > snapshots > line variant 1`] = `"
Content 1
"`; -exports[`Tabs > snapshots > tabs with three tabs 1`] = `"
Overview content
"`; +exports[`Tabs > snapshots > tabs with disabled tab 1`] = `"
Content 1
"`; + +exports[`Tabs > snapshots > tabs with three tabs 1`] = `"
Overview content
"`; diff --git a/packages/core/src/components/tabs.test.tsx b/packages/core/src/components/tabs.test.tsx index e7191048..c76867f3 100644 --- a/packages/core/src/components/tabs.test.tsx +++ b/packages/core/src/components/tabs.test.tsx @@ -58,6 +58,20 @@ describe("Tabs", () => { ); expect(container.innerHTML).toMatchSnapshot(); }); + + it("line variant", () => { + const { container } = render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + expect(container.innerHTML).toMatchSnapshot(); + }); }); it("renders all tabs", () => { diff --git a/packages/core/src/components/tabs.tsx b/packages/core/src/components/tabs.tsx index a328c008..9b82156b 100644 --- a/packages/core/src/components/tabs.tsx +++ b/packages/core/src/components/tabs.tsx @@ -3,6 +3,10 @@ import { Tabs as BaseTabs } from "@base-ui/react/tabs"; import { cn } from "@/lib/utils"; +type TabsVariant = "default" | "line"; + +const TabsVariantContext = React.createContext("default"); + // Only the props relevant to the Tabs abstraction are picked from BaseTabs.Root. // Base UI-internal props are intentionally excluded so that upstream changes // don't leak as breaking changes to consumers. @@ -12,6 +16,7 @@ type RootProps = Pick< > & { children: React.ReactNode; className?: string; + variant?: TabsVariant; }; /** @@ -31,22 +36,28 @@ type RootProps = Pick< * * ``` */ -function Root({ className, children, ...props }: RootProps) { +function Root({ className, children, variant = "default", ...props }: RootProps) { return ( - - {children} - + + + {children} + + ); } Root.displayName = "Tabs.Root"; /** Groups the individual tab buttons. */ function List({ className, children, ...props }: React.ComponentProps) { + const variant = React.useContext(TabsVariantContext); return ( ) { + const variant = React.useContext(TabsVariantContext); return ( Date: Mon, 25 May 2026 12:15:16 +0900 Subject: [PATCH 3/7] fix: update tabs snapshot to match bg-muted change --- .../core/__snapshots__/src__components__tabs.test.tsx.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/__snapshots__/src__components__tabs.test.tsx.snap b/packages/core/__snapshots__/src__components__tabs.test.tsx.snap index 8259ccb3..50551870 100644 --- a/packages/core/__snapshots__/src__components__tabs.test.tsx.snap +++ b/packages/core/__snapshots__/src__components__tabs.test.tsx.snap @@ -1,9 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Tabs > snapshots > default tabs 1`] = `"
Content 1
"`; +exports[`Tabs > snapshots > default tabs 1`] = `"
Content 1
"`; exports[`Tabs > snapshots > line variant 1`] = `"
Content 1
"`; -exports[`Tabs > snapshots > tabs with disabled tab 1`] = `"
Content 1
"`; +exports[`Tabs > snapshots > tabs with disabled tab 1`] = `"
Content 1
"`; -exports[`Tabs > snapshots > tabs with three tabs 1`] = `"
Overview content
"`; +exports[`Tabs > snapshots > tabs with three tabs 1`] = `"
Overview content
"`; From 7f6efc1076228393ccaadf0ba1d872b834b4ca06 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 25 May 2026 12:23:32 +0900 Subject: [PATCH 4/7] Add tabs component in primitive demos --- docs/components/tabs.md | 18 +++ .../src/modules/pages/primitives-demo.tsx | 36 +++++ examples/vite-app/src/pages/settings/page.tsx | 125 +++++++----------- 3 files changed, 102 insertions(+), 77 deletions(-) diff --git a/docs/components/tabs.md b/docs/components/tabs.md index 45cb545a..31ae2144 100644 --- a/docs/components/tabs.md +++ b/docs/components/tabs.md @@ -47,6 +47,7 @@ import { Tabs } from "@tailor-platform/app-shell"; | `value` | `Tabs.Tab.Value` | - | Controlled active tab value | | `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes | | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Orientation of the tabs | +| `variant` | `'default' \| 'line'` | `'default'` | Visual style of the tabs | | `className` | `string` | - | Additional CSS classes for root | | `children` | `React.ReactNode` | - | Tabs sub-components | @@ -105,3 +106,20 @@ const [activeTab, setActiveTab] = useState("overview"); Archived items ``` + +### Line Variant + +The `line` variant renders tabs with an underline indicator instead of a background highlight. + +```tsx + + + Overview + Activity + Settings + + Overview content + Activity content + Settings content + +``` diff --git a/examples/nextjs-app/src/modules/pages/primitives-demo.tsx b/examples/nextjs-app/src/modules/pages/primitives-demo.tsx index 52549cc2..113826ab 100644 --- a/examples/nextjs-app/src/modules/pages/primitives-demo.tsx +++ b/examples/nextjs-app/src/modules/pages/primitives-demo.tsx @@ -10,6 +10,7 @@ import { Sheet, Menu, Table, + Tabs, } from "@tailor-platform/app-shell"; import * as React from "react"; @@ -332,6 +333,41 @@ export const primitiveComponentsDemoResource = defineResource({ + {/* Tabs */} + + + +
+
+
Default
+ + + Overview + Projects + Settings + + Overview content + Projects content + Settings content + +
+
+
Line
+ + + Activity + Members + Billing + + Activity content + Members content + Billing content + +
+
+
+
+ {/* Table */} diff --git a/examples/vite-app/src/pages/settings/page.tsx b/examples/vite-app/src/pages/settings/page.tsx index 6af6cad9..5f112e41 100644 --- a/examples/vite-app/src/pages/settings/page.tsx +++ b/examples/vite-app/src/pages/settings/page.tsx @@ -1,13 +1,5 @@ import { useState } from "react"; -import { - Layout, - Input, - Button, - Dialog, - Tabs, - type AppShellPageProps, - Card, -} from "@tailor-platform/app-shell"; +import { Layout, Input, Button, Dialog, type AppShellPageProps } from "@tailor-platform/app-shell"; import { labels } from "../../i18n-labels"; const SettingsIcon = () => ( @@ -35,77 +27,56 @@ const SettingsPage = () => { -

+

This page is at{" "} src/pages/settings/page.tsx

- - - - - General - Notifications - Account - - -
-
- - setName(e.target.value)} /> -
-
- - setEmail(e.target.value)} - /> -
-
- - }>Save Changes - - - Confirm Changes - - Save "{name}" as the application name and " - {email}" as the admin email? - - - - }>Cancel - }>Confirm - - - - -
-
-
- -

- Configure your notification preferences here. -

-
- -

Manage your account settings here.

-
-
-
-
+
+
+ + setName(e.target.value)} /> +
+
+ + setEmail(e.target.value)} + /> +
+
+ + }>Save Changes + + + Confirm Changes + + Save "{name}" as the application name and " + {email}" as the admin email? + + + + }>Cancel + }>Confirm + + + + +
+
); From d8c521beb4d2a1a524556e623cdba8cf14a63719 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 25 May 2026 12:26:47 +0900 Subject: [PATCH 5/7] Remove orientation prop --- docs/components/tabs.md | 17 ++++++++--------- packages/core/src/components/tabs.tsx | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/components/tabs.md b/docs/components/tabs.md index 31ae2144..c20935eb 100644 --- a/docs/components/tabs.md +++ b/docs/components/tabs.md @@ -41,15 +41,14 @@ import { Tabs } from "@tailor-platform/app-shell"; ### Tabs.Root Props -| Prop | Type | Default | Description | -| --------------- | ---------------------------- | -------------- | --------------------------------------- | -| `defaultValue` | `Tabs.Tab.Value` | `0` | Initial active tab value (uncontrolled) | -| `value` | `Tabs.Tab.Value` | - | Controlled active tab value | -| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes | -| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Orientation of the tabs | -| `variant` | `'default' \| 'line'` | `'default'` | Visual style of the tabs | -| `className` | `string` | - | Additional CSS classes for root | -| `children` | `React.ReactNode` | - | Tabs sub-components | +| Prop | Type | Default | Description | +| --------------- | ---------------------- | ----------- | --------------------------------------- | +| `defaultValue` | `Tabs.Tab.Value` | `0` | Initial active tab value (uncontrolled) | +| `value` | `Tabs.Tab.Value` | - | Controlled active tab value | +| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes | +| `variant` | `'default' \| 'line'` | `'default'` | Visual style of the tabs | +| `className` | `string` | - | Additional CSS classes for root | +| `children` | `React.ReactNode` | - | Tabs sub-components | ### Tabs.List Props diff --git a/packages/core/src/components/tabs.tsx b/packages/core/src/components/tabs.tsx index 9b82156b..0e1fa48a 100644 --- a/packages/core/src/components/tabs.tsx +++ b/packages/core/src/components/tabs.tsx @@ -12,7 +12,7 @@ const TabsVariantContext = React.createContext("default"); // don't leak as breaking changes to consumers. type RootProps = Pick< React.ComponentProps, - "defaultValue" | "value" | "onValueChange" | "orientation" + "defaultValue" | "value" | "onValueChange" > & { children: React.ReactNode; className?: string; From dc6b4361eb167dae772bf0129cd1109061db59e2 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 25 May 2026 17:19:02 +0900 Subject: [PATCH 6/7] Add capsule variant --- docs/components/tabs.md | 33 +++++-------------- .../src/modules/pages/primitives-demo.tsx | 15 +++++++++ .../src__components__tabs.test.tsx.snap | 2 ++ packages/core/src/components/tabs.test.tsx | 14 ++++++++ packages/core/src/components/tabs.tsx | 10 ++++-- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/docs/components/tabs.md b/docs/components/tabs.md index c20935eb..d5d6d134 100644 --- a/docs/components/tabs.md +++ b/docs/components/tabs.md @@ -41,14 +41,14 @@ import { Tabs } from "@tailor-platform/app-shell"; ### Tabs.Root Props -| Prop | Type | Default | Description | -| --------------- | ---------------------- | ----------- | --------------------------------------- | -| `defaultValue` | `Tabs.Tab.Value` | `0` | Initial active tab value (uncontrolled) | -| `value` | `Tabs.Tab.Value` | - | Controlled active tab value | -| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes | -| `variant` | `'default' \| 'line'` | `'default'` | Visual style of the tabs | -| `className` | `string` | - | Additional CSS classes for root | -| `children` | `React.ReactNode` | - | Tabs sub-components | +| Prop | Type | Default | Description | +| --------------- | ---------------------------------- | ----------- | --------------------------------------- | +| `defaultValue` | `Tabs.Tab.Value` | `0` | Initial active tab value (uncontrolled) | +| `value` | `Tabs.Tab.Value` | - | Controlled active tab value | +| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes | +| `variant` | `'default' \| 'line' \| 'capsule'` | `'default'` | Visual style of the tabs | +| `className` | `string` | - | Additional CSS classes for root | +| `children` | `React.ReactNode` | - | Tabs sub-components | ### Tabs.List Props @@ -105,20 +105,3 @@ const [activeTab, setActiveTab] = useState("overview"); Archived items ``` - -### Line Variant - -The `line` variant renders tabs with an underline indicator instead of a background highlight. - -```tsx - - - Overview - Activity - Settings - - Overview content - Activity content - Settings content - -``` diff --git a/examples/nextjs-app/src/modules/pages/primitives-demo.tsx b/examples/nextjs-app/src/modules/pages/primitives-demo.tsx index 113826ab..c47bb9a7 100644 --- a/examples/nextjs-app/src/modules/pages/primitives-demo.tsx +++ b/examples/nextjs-app/src/modules/pages/primitives-demo.tsx @@ -364,6 +364,21 @@ export const primitiveComponentsDemoResource = defineResource({ Billing content +
+
Capsule
+ + + All + Open + Received + Closed + + All content + Open content + Received content + Closed content + +
diff --git a/packages/core/__snapshots__/src__components__tabs.test.tsx.snap b/packages/core/__snapshots__/src__components__tabs.test.tsx.snap index 50551870..d445a73f 100644 --- a/packages/core/__snapshots__/src__components__tabs.test.tsx.snap +++ b/packages/core/__snapshots__/src__components__tabs.test.tsx.snap @@ -1,5 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`Tabs > snapshots > capsule variant 1`] = `"
Content 1
"`; + exports[`Tabs > snapshots > default tabs 1`] = `"
Content 1
"`; exports[`Tabs > snapshots > line variant 1`] = `"
Content 1
"`; diff --git a/packages/core/src/components/tabs.test.tsx b/packages/core/src/components/tabs.test.tsx index c76867f3..ffc98aa2 100644 --- a/packages/core/src/components/tabs.test.tsx +++ b/packages/core/src/components/tabs.test.tsx @@ -72,6 +72,20 @@ describe("Tabs", () => { ); expect(container.innerHTML).toMatchSnapshot(); }); + + it("capsule variant", () => { + const { container } = render( + + + Tab 1 + Tab 2 + + Content 1 + Content 2 + , + ); + expect(container.innerHTML).toMatchSnapshot(); + }); }); it("renders all tabs", () => { diff --git a/packages/core/src/components/tabs.tsx b/packages/core/src/components/tabs.tsx index 0e1fa48a..4afe385f 100644 --- a/packages/core/src/components/tabs.tsx +++ b/packages/core/src/components/tabs.tsx @@ -3,7 +3,7 @@ import { Tabs as BaseTabs } from "@base-ui/react/tabs"; import { cn } from "@/lib/utils"; -type TabsVariant = "default" | "line"; +type TabsVariant = "default" | "line" | "capsule"; const TabsVariantContext = React.createContext("default"); @@ -57,7 +57,9 @@ function List({ className, children, ...props }: React.ComponentProps Date: Mon, 25 May 2026 17:34:03 +0900 Subject: [PATCH 7/7] Add examples: badges and icons --- docs/components/tabs.md | 21 ++ .../src/modules/pages/primitives-demo.tsx | 183 ++++++++++++++---- 2 files changed, 164 insertions(+), 40 deletions(-) diff --git a/docs/components/tabs.md b/docs/components/tabs.md index d5d6d134..fed9be3e 100644 --- a/docs/components/tabs.md +++ b/docs/components/tabs.md @@ -105,3 +105,24 @@ const [activeTab, setActiveTab] = useState("overview"); Archived items ``` + +### Use Icons in Tab + +```tsx + + + + + + + + + + + + + Overview content + Projects content + Settings content + +``` diff --git a/examples/nextjs-app/src/modules/pages/primitives-demo.tsx b/examples/nextjs-app/src/modules/pages/primitives-demo.tsx index c47bb9a7..8cef8fe2 100644 --- a/examples/nextjs-app/src/modules/pages/primitives-demo.tsx +++ b/examples/nextjs-app/src/modules/pages/primitives-demo.tsx @@ -14,6 +14,61 @@ import { } from "@tailor-platform/app-shell"; import * as React from "react"; +const LayoutDashboardIcon = () => ( + + + + + + +); + +const FolderKanbanIcon = () => ( + + + + + + +); + +const SettingsIcon = () => ( + + + + +); + export const primitiveComponentsDemoResource = defineResource({ path: "primitives-demo", meta: { @@ -337,47 +392,95 @@ export const primitiveComponentsDemoResource = defineResource({ -
-
-
Default
- - - Overview - Projects - Settings - - Overview content - Projects content - Settings content - -
-
-
Line
- - - Activity - Members - Billing - - Activity content - Members content - Billing content - +
+
+
+
Default
+ + + Overview + Projects + Settings + + Overview content + Projects content + Settings content + +
+
+
Line
+ + + Activity + Members + Billing + + Activity content + Members content + Billing content + +
+
+
Capsule
+ + + All + Open + Received + Closed + + All content + Open content + Received content + Closed content + +
-
-
Capsule
- - - All - Open - Received - Closed - - All content - Open content - Received content - Closed content - +
+
+
With Badge
+ + + + Overview + + New + + + + Projects + 10+ + + + Overview content + Projects content + +
+
+
Icons
+ + + + + + + + + + + + + Overview content + Projects content + Settings content + +