From cd55ebe9f82634d4e2e2c34decca7c25e160657c Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Mon, 12 Oct 2026 08:21:42 +0200 Subject: [PATCH 1/8] feat(ui): add TabBar appearance="buttons", rename "content" to "tabs" (#2028) - Add appearance="buttons" for a pill-style segmented control - Add appearance="tabs" as new canonical name (replaces "content") - Keep appearance="content" as deprecated functional alias with console.warn - Preserve appearance="main" as default (unchanged) - Add CSS tokens for buttons appearance in theme.css and global.css - Hide deprecated tabStyle prop from Storybook Controls Signed-off-by: MartinS-git --- .changeset/tabbar-appearance.md | 10 +++ .../components/TabBar/TabBar.component.tsx | 26 +++++-- .../src/components/TabBar/TabBar.stories.tsx | 51 ++++++++++++-- .../src/components/TabBar/TabBar.test.tsx | 56 ++++++++++++++- .../TabBar/TabNavigation.deprecated.tsx | 8 +-- .../TabBarItem/TabBarItem.component.tsx | 69 +++++++++++++++++-- .../components/TabBarItem/TabBarItem.test.tsx | 32 ++++++++- packages/ui-components/src/global.css | 20 ++++++ packages/ui-components/src/index.ts | 1 + packages/ui-components/src/theme.css | 20 ++++++ 10 files changed, 270 insertions(+), 23 deletions(-) create mode 100644 .changeset/tabbar-appearance.md diff --git a/.changeset/tabbar-appearance.md b/.changeset/tabbar-appearance.md new file mode 100644 index 0000000000..bfc15cc4c6 --- /dev/null +++ b/.changeset/tabbar-appearance.md @@ -0,0 +1,10 @@ +--- +"@cloudoperators/juno-ui-components": minor +--- + +feat(ui): add TabBar `appearance="buttons"` and rename `appearance="content"` to `appearance="tabs"` + +- Add `appearance="buttons"` to `TabBar` for a pill-style segmented control, useful for pre-defined filters. +- Rename `appearance="content"` to `appearance="tabs"`. The existing default `appearance="main"` is unchanged. +- `appearance="content"` keeps working as a deprecated alias of `tabs` and emits a deprecation warning. Switch to `appearance="tabs"`. +- Disabled items in the `tabs`/`content` appearance no longer dim the shared bottom border. diff --git a/packages/ui-components/src/components/TabBar/TabBar.component.tsx b/packages/ui-components/src/components/TabBar/TabBar.component.tsx index d0ca709222..0705a5d173 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.component.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.component.tsx @@ -3,14 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, HTMLAttributes, ReactNode } from "react" +import React, { createContext, HTMLAttributes, ReactNode, useEffect, useRef } from "react" import { Navigation } from "../Navigation/Navigation.component" const tabBarStyles = ` jn:flex ` -export type TabBarAppearance = "main" | "content" +const tabBarButtonsStyles = ` + jn:bg-theme-tab-buttons + jn:rounded-[0.25rem] + jn:p-[0.125rem] +` + +/** @deprecated Use `"tabs"` instead. `"content"` remains functional but will be removed in a future major release. */ +export type DeprecatedTabBarAppearance = "content" + +export type TabBarAppearance = "main" | "tabs" | "buttons" | DeprecatedTabBarAppearance /** @deprecated Use TabBarAppearance instead. Can be removed when TabNavigation is removed. */ export type TabStyle = TabBarAppearance @@ -47,6 +56,15 @@ export const TabBar = ({ ...props }: TabBarProps): ReactNode => { const resolvedAppearance = appearance || tabStyle || "main" // `|| tabStyle` can be removed when TabNavigation is removed + + const warnedDeprecatedContent = useRef(false) + useEffect(() => { + if (resolvedAppearance === "content" && !warnedDeprecatedContent.current) { + warnedDeprecatedContent.current = true + console.warn('TabBar: appearance="content" is deprecated. Use appearance="tabs" instead.') + } + }, [resolvedAppearance]) + return ( { /** A handler to execute when the active tab changes */ // eslint-disable-next-line no-unused-vars onActiveItemChange?: (activeItem: ReactNode) => void - /** The visual appearance of the TabBar. Use `main` as the first child in an AppShell. Use `content` for tabs inside page content — adds a darkened bottom border on inactive tabs. */ + /** The visual appearance of the TabBar. Defaults to `main`. Use `main` as the first child in an AppShell. Use `tabs` for tabs inside page content — adds a darkened bottom border on inactive tabs. Use `buttons` for a pill-style segmented control. `content` is deprecated — use `tabs` instead. */ appearance?: TabBarAppearance /** @deprecated Use appearance instead. Can be removed when TabNavigation is removed. */ tabStyle?: TabBarAppearance diff --git a/packages/ui-components/src/components/TabBar/TabBar.stories.tsx b/packages/ui-components/src/components/TabBar/TabBar.stories.tsx index 336c8da503..00138ea7d9 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.stories.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.stories.tsx @@ -19,9 +19,12 @@ const meta: Meta = { control: false, }, appearance: { - options: ["main", "content"], + options: ["main", "tabs", "buttons"], control: { type: "radio" }, }, + tabStyle: { + table: { disable: true }, + }, }, } @@ -105,17 +108,57 @@ export const WithChildren: Story = { }, } -export const ContentAppearance: Story = { +export const MainAppearance: Story = { + parameters: { + docs: { + description: { + story: + 'Use `appearance="main"` for the primary navigation as the first child in an AppShell. Each item carries its own bottom border, highlighted on the active item.', + }, + }, + }, + args: { + appearance: "main", + children: [ + , + , + , + , + ], + }, +} + +export const TabsAppearance: Story = { + parameters: { + docs: { + description: { + story: + 'Use `appearance="tabs"` for a TabBar inside page content. Inactive items show a darkened bottom border to visually distinguish them from the active item.', + }, + }, + }, + args: { + appearance: "tabs", + children: [ + , + , + , + , + ], + }, +} + +export const ButtonsAppearance: Story = { parameters: { docs: { description: { story: - 'Use `appearance="content"` for a TabBar inside page content. Inactive items show a darkened bottom border to visually distinguish them from the active item.', + 'Use `appearance="buttons"` for a pill-style segmented control. Items are displayed as buttons with a shared background container.', }, }, }, args: { - appearance: "content", + appearance: "buttons", children: [ , , diff --git a/packages/ui-components/src/components/TabBar/TabBar.test.tsx b/packages/ui-components/src/components/TabBar/TabBar.test.tsx index a4afd0dbb4..d4348c8091 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.test.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.test.tsx @@ -177,7 +177,42 @@ describe("TabBar", () => { expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-main") }) - test("renders content appearance as passed via appearance prop", async () => { + test("renders main appearance as passed via appearance prop", async () => { + await waitFor(() => + render( + + + + ) + ) + expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-main") + }) + + test("renders tabs appearance as passed via appearance prop", async () => { + await waitFor(() => + render( + + + + ) + ) + expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-tabs") + }) + + test("renders buttons appearance as passed via appearance prop", async () => { + await waitFor(() => + render( + + + + ) + ) + expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-buttons") + }) + + // Can be removed when the deprecated "content" appearance is removed: + test("still renders the deprecated content appearance as passed via appearance prop", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) await waitFor(() => render( @@ -186,6 +221,21 @@ describe("TabBar", () => { ) ) expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-content") + warnSpy.mockRestore() + }) + + // Can be removed when the deprecated "content" appearance is removed: + test("emits a deprecation warning when the content appearance is used", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + await waitFor(() => + render( + + + + ) + ) + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('appearance="content" is deprecated')) + warnSpy.mockRestore() }) // Can be removed when TabNavigation is removed: @@ -220,11 +270,11 @@ describe("TabBar", () => { return null } render( - + ) - expect(ctx?.appearance).toBe("content") + expect(ctx?.appearance).toBe("tabs") }) // Can be removed when TabNavigation is removed: diff --git a/packages/ui-components/src/components/TabBar/TabNavigation.deprecated.tsx b/packages/ui-components/src/components/TabBar/TabNavigation.deprecated.tsx index 92f4f1ced4..0b3e480b10 100644 --- a/packages/ui-components/src/components/TabBar/TabNavigation.deprecated.tsx +++ b/packages/ui-components/src/components/TabBar/TabNavigation.deprecated.tsx @@ -10,12 +10,12 @@ import { TabBar, TabBarProps } from "./TabBar.component" import { withDeprecationWarning } from "../withDeprecationWarning/index" const TabNavigationBackwardsCompat = ({ appearance, tabStyle, className = "", ...props }: TabBarProps) => { - const a = appearance || tabStyle || "main" + // Preserve the historic TabNavigation default of "main" rather than TabBar's new "tabs" default. + const resolvedAppearance = appearance || tabStyle || "main" return ( ) diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx index 6ae58164ab..68a8285bb2 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx @@ -28,6 +28,58 @@ const tabBarActiveItemStyles = ` jn:border-theme-tab-active-bottom ` +const tabBarItemButtonStyles = ` + jn:rounded-[0.1875rem] + jn:px-[0.625rem] + jn:py-[0.4375rem] + jn:text-sm + jn:font-bold + jn:leading-[1.4] + jn:border + jn:cursor-pointer + jn:transition-colors + jn:duration-150 + jn:focus-visible:outline-hidden + jn:focus-visible:ring-2 + jn:focus-visible:ring-theme-focus + jn:focus-visible:ring-offset-1 +` + +const tabBarItemButtonActiveStyles = ` + jn:bg-theme-tab-button-active + jn:text-theme-tab-button-active + jn:border-theme-tab-button-active +` + +const tabBarItemButtonInactiveStyles = ` + jn:text-theme-tab-button + jn:border-transparent + jn:hover:text-theme-tab-button-hover +` + +const tabBarItemTabsStyles = ` + jn:flex + jn:items-center + jn:text-theme-default + jn:font-bold + jn:py-[0.875rem] + jn:px-[1.5625rem] + jn:focus-visible:outline-hidden + jn:focus-visible:ring-2 + jn:focus-visible:ring-theme-focus +` + +const tabBarActiveTabsItemStyles = ` + jn:text-theme-high + jn:font-bold +` + +const tabBarItemTabsWrapperStyles = ` + jn:border-b-[3px] + jn:border-theme-tab-content-inactive-bottom + jn:has-[.juno-navigation-item-active]:border-theme-tab-active-bottom +` + /** * An individual TabBar item. Use wrapped in a `` parent component. * @see https://cloudoperators.github.io/juno/?path=/docs/navigation-tabbar-tabbaritem--docs @@ -46,27 +98,30 @@ export const TabBarItem = ({ value = "", ...props }: TabBarItemProps): ReactNode => { - const tabBarContext = useContext(TabBarContext) - const { appearance } = tabBarContext || {} - const resolvedAppearance = appearance + const { appearance } = useContext(TabBarContext) || {} + const isButtons = appearance === "buttons" + const isTabsLike = appearance === "tabs" || appearance === "content" // "content" is the deprecated alias of "tabs" return ( {children} diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.test.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.test.tsx index 7c67dcea84..a1eba0a44f 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.test.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.test.tsx @@ -87,13 +87,43 @@ describe("TabBarItem", () => { expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-main-item") }) - test("renders content appearance items as passed to the parent TabBar via appearance prop", () => { + test("renders main appearance items as passed to the parent TabBar via appearance prop", () => { + render( + + + + ) + expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-main-item") + }) + + test("renders tabs appearance items as passed to the parent TabBar via appearance prop", () => { + render( + + + + ) + expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-tabs-item") + }) + + test("renders buttons appearance items as passed to the parent TabBar via appearance prop", () => { + render( + + + + ) + expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-buttons-item") + }) + + // Can be removed when the deprecated "content" appearance is removed: + test("still renders the deprecated content appearance items via appearance prop", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) render( ) expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-content-item") + warnSpy.mockRestore() }) // Can be removed when TabNavigation is removed: diff --git a/packages/ui-components/src/global.css b/packages/ui-components/src/global.css index 10f804dc53..eb069a88e0 100644 --- a/packages/ui-components/src/global.css +++ b/packages/ui-components/src/global.css @@ -386,6 +386,8 @@ --background-color-theme-switch-handle: var(--color-switch-handle-bg); --background-color-theme-switch-handle-checked: var(--color-switch-handle-checked-bg); --background-color-theme-tab-navigation-top: var(--color-tabnavigation-top-bg); + --background-color-theme-tab-buttons: var(--color-tab-buttons-bg); + --background-color-theme-tab-button-active: var(--color-tab-button-bg-active); --background-color-theme-textinput: var(--color-textinput-bg); --background-color-theme-textinput-autofill: var(--color-textinput-autofill-bg); --background-color-theme-topnavigation-item: var(--color-topnavigation-item-bg); @@ -428,6 +430,9 @@ --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active); --text-color-theme-status-details: var(--color-status-details-text); --text-color-theme-status-code: var(--color-status-code); + --text-color-theme-tab-button: var(--color-tab-button-text); + --text-color-theme-tab-button-active: var(--color-tab-button-text-active); + --text-color-theme-tab-button-hover: var(--color-tab-button-text-hover); /* Component Border Colors: */ --border-color-theme-default: var(--color-border-default); @@ -470,6 +475,7 @@ --border-color-theme-tab-navigation-content-bottom: var(--color-tabnavigation-content-bottom-border); --border-color-theme-tab-active-bottom: var(--color-text-default); + --border-color-theme-tab-button-active: var(--color-tab-button-border-active); --border-color-theme-tab-content-inactive-bottom: var(--color-background-lvl-4); --border-color-theme-textinput-default: var(--color-textinput-default-border); @@ -755,6 +761,13 @@ /* LT TabNavigation */ --color-tabnavigation-top-bg: var(--color-juno-grey-light-5); --color-tabnavigation-content-bottom-border: var(--color-background-lvl-4); + /* LT TabBar Buttons */ + --color-tab-buttons-bg: var(--color-juno-grey-light-3); + --color-tab-button-bg-active: var(--color-white); + --color-tab-button-border-active: var(--color-juno-grey-light-9); + --color-tab-button-text: var(--color-text-default); + --color-tab-button-text-active: var(--color-text-high); + --color-tab-button-text-hover: var(--color-text-highest); /* LT Filters */ --color-filters-bg: var(--color-background-lvl-1); --color-filter-input-bg: var(--color-background-lvl-0); @@ -1023,6 +1036,13 @@ /* DT TabNavigation */ --color-tabnavigation-top-bg: var(--color-juno-grey-blue-8); --color-tabnavigation-content-bottom-border: var(--color-background-lvl-4); + /* DT TabBar Buttons */ + --color-tab-buttons-bg: var(--color-background-lvl-2); + --color-tab-button-bg-active: var(--color-background-lvl-0); + --color-tab-button-border-active: var(--color-border-default); + --color-tab-button-text: var(--color-text-default); + --color-tab-button-text-active: var(--color-text-high); + --color-tab-button-text-hover: var(--color-text-highest); /* DT Filters */ --color-filters-bg: var(--color-background-lvl-1); --color-filter-input-bg: var(--color-background-lvl-0); diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts index 6e9c85e1f3..6202c0ec63 100644 --- a/packages/ui-components/src/index.ts +++ b/packages/ui-components/src/index.ts @@ -237,6 +237,7 @@ export type { TabListProps } from "./components/TabList/TabList.component" export type { TabBarProps, TabBarAppearance, + DeprecatedTabBarAppearance, TabBarContextType, TabNavigationProps, // Can be removed when TabNavigation is removed TabNavigationContextType, // Can be removed when TabNavigation is removed diff --git a/packages/ui-components/src/theme.css b/packages/ui-components/src/theme.css index 5862d61c03..496cd58dc7 100644 --- a/packages/ui-components/src/theme.css +++ b/packages/ui-components/src/theme.css @@ -378,6 +378,8 @@ --background-color-theme-switch-handle: var(--color-switch-handle-bg); --background-color-theme-switch-handle-checked: var(--color-switch-handle-checked-bg); --background-color-theme-tab-navigation-top: var(--color-tabnavigation-top-bg); + --background-color-theme-tab-buttons: var(--color-tab-buttons-bg); + --background-color-theme-tab-button-active: var(--color-tab-button-bg-active); --background-color-theme-textinput: var(--color-textinput-bg); --background-color-theme-textinput-autofill: var(--color-textinput-autofill-bg); --background-color-theme-topnavigation-item: var(--color-topnavigation-item-bg); @@ -410,6 +412,9 @@ --text-color-theme-sidenavigation-item-active: var(--color-sidenavigation-item-active); --text-color-theme-status-details: var(--color-status-details-text); --text-color-theme-status-code: var(--color-status-code); + --text-color-theme-tab-button: var(--color-tab-button-text); + --text-color-theme-tab-button-active: var(--color-tab-button-text-active); + --text-color-theme-tab-button-hover: var(--color-tab-button-text-hover); /* Component Border Colors: */ --border-color-theme-default: var(--color-border-default); @@ -457,6 +462,7 @@ --border-color-theme-tab-navigation-content-bottom: var(--color-tabnavigation-content-bottom-border); --border-color-theme-tab-active-bottom: var(--color-text-default); + --border-color-theme-tab-button-active: var(--color-tab-button-border-active); --border-color-theme-tab-content-inactive-bottom: var(--color-background-lvl-4); --border-color-theme-textinput-default: var(--color-textinput-default-border); @@ -729,6 +735,13 @@ /* LT TabNavigation */ --color-tabnavigation-top-bg: var(--color-juno-grey-light-5); --color-tabnavigation-content-bottom-border: var(--color-background-lvl-4); + /* LT TabBar Buttons */ + --color-tab-buttons-bg: var(--color-juno-grey-light-3); + --color-tab-button-bg-active: var(--color-white); + --color-tab-button-border-active: var(--color-juno-grey-light-9); + --color-tab-button-text: var(--color-text-default); + --color-tab-button-text-active: var(--color-text-high); + --color-tab-button-text-hover: var(--color-text-highest); /* LT Filters */ --color-filters-bg: var(--color-background-lvl-1); --color-filter-input-bg: var(--color-background-lvl-0); @@ -1011,6 +1024,13 @@ /* DT TabNavigation */ --color-tabnavigation-top-bg: var(--color-juno-grey-blue-8); --color-tabnavigation-content-bottom-border: var(--color-background-lvl-4); + /* DT TabBar Buttons */ + --color-tab-buttons-bg: var(--color-background-lvl-2); + --color-tab-button-bg-active: var(--color-background-lvl-0); + --color-tab-button-border-active: var(--color-border-default); + --color-tab-button-text: var(--color-text-default); + --color-tab-button-text-active: var(--color-text-high); + --color-tab-button-text-hover: var(--color-text-highest); /* DT Filters */ --color-filters-bg: var(--color-background-lvl-1); --color-filter-input-bg: var(--color-background-lvl-0); From f2151ff4ce8328bd304b0b3a669f5c8f66c0750a Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Mon, 12 Oct 2026 10:12:52 +0200 Subject: [PATCH 2/8] feat(ui): rename appearance="main" to "tabs", keep "content" as distinct variant - Default changes from "main" to "tabs" - "main" is now a deprecated alias of "tabs" with console.warn - "content" remains a distinct style variant (no longer deprecated) - Update stories, tests, and changeset accordingly Signed-off-by: MartinS-git --- .changeset/tabbar-appearance.md | 8 ++--- .../components/TabBar/TabBar.component.tsx | 14 ++++----- .../src/components/TabBar/TabBar.stories.tsx | 26 +++++++++++++++-- .../src/components/TabBar/TabBar.test.tsx | 29 +++++++++++++------ .../TabBarItem/TabBarItem.component.tsx | 2 +- .../components/TabBarItem/TabBarItem.test.tsx | 21 ++++++++++---- 6 files changed, 71 insertions(+), 29 deletions(-) diff --git a/.changeset/tabbar-appearance.md b/.changeset/tabbar-appearance.md index bfc15cc4c6..462e88283a 100644 --- a/.changeset/tabbar-appearance.md +++ b/.changeset/tabbar-appearance.md @@ -2,9 +2,9 @@ "@cloudoperators/juno-ui-components": minor --- -feat(ui): add TabBar `appearance="buttons"` and rename `appearance="content"` to `appearance="tabs"` +feat(ui): add TabBar `appearance="buttons"` and rename `appearance="main"` to `appearance="tabs"` - Add `appearance="buttons"` to `TabBar` for a pill-style segmented control, useful for pre-defined filters. -- Rename `appearance="content"` to `appearance="tabs"`. The existing default `appearance="main"` is unchanged. -- `appearance="content"` keeps working as a deprecated alias of `tabs` and emits a deprecation warning. Switch to `appearance="tabs"`. -- Disabled items in the `tabs`/`content` appearance no longer dim the shared bottom border. +- Rename `appearance="main"` to `appearance="tabs"`. `appearance="tabs"` is now the default. +- `appearance="main"` keeps working as a deprecated alias and emits a deprecation warning. Switch to `appearance="tabs"`. +- `appearance="content"` remains a distinct style variant with a shared bottom border (unchanged). diff --git a/packages/ui-components/src/components/TabBar/TabBar.component.tsx b/packages/ui-components/src/components/TabBar/TabBar.component.tsx index 0705a5d173..0dfa4b1483 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.component.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.component.tsx @@ -16,10 +16,10 @@ const tabBarButtonsStyles = ` jn:p-[0.125rem] ` -/** @deprecated Use `"tabs"` instead. `"content"` remains functional but will be removed in a future major release. */ -export type DeprecatedTabBarAppearance = "content" +/** @deprecated Use `"tabs"` instead. `"main"` remains functional but will be removed in a future major release. */ +export type DeprecatedTabBarAppearance = "main" -export type TabBarAppearance = "main" | "tabs" | "buttons" | DeprecatedTabBarAppearance +export type TabBarAppearance = "tabs" | "buttons" | "content" | DeprecatedTabBarAppearance /** @deprecated Use TabBarAppearance instead. Can be removed when TabNavigation is removed. */ export type TabStyle = TabBarAppearance @@ -55,13 +55,13 @@ export const TabBar = ({ tabStyle, // Can be removed when TabNavigation is removed ...props }: TabBarProps): ReactNode => { - const resolvedAppearance = appearance || tabStyle || "main" // `|| tabStyle` can be removed when TabNavigation is removed + const resolvedAppearance = appearance || tabStyle || "tabs" // `|| tabStyle` can be removed when TabNavigation is removed const warnedDeprecatedContent = useRef(false) useEffect(() => { - if (resolvedAppearance === "content" && !warnedDeprecatedContent.current) { + if (resolvedAppearance === "main" && !warnedDeprecatedContent.current) { warnedDeprecatedContent.current = true - console.warn('TabBar: appearance="content" is deprecated. Use appearance="tabs" instead.') + console.warn('TabBar: appearance="main" is deprecated. Use appearance="tabs" instead.') } }, [resolvedAppearance]) @@ -100,7 +100,7 @@ export interface TabBarProps extends HTMLAttributes { /** A handler to execute when the active tab changes */ // eslint-disable-next-line no-unused-vars onActiveItemChange?: (activeItem: ReactNode) => void - /** The visual appearance of the TabBar. Defaults to `main`. Use `main` as the first child in an AppShell. Use `tabs` for tabs inside page content — adds a darkened bottom border on inactive tabs. Use `buttons` for a pill-style segmented control. `content` is deprecated — use `tabs` instead. */ + /** The visual appearance of the TabBar. Defaults to `tabs`. Use `tabs` for tabs inside an AppShell or page content. Use `buttons` for a pill-style segmented control. `content` is a distinct style variant with a shared bottom border. `main` is deprecated — use `tabs` instead. */ appearance?: TabBarAppearance /** @deprecated Use appearance instead. Can be removed when TabNavigation is removed. */ tabStyle?: TabBarAppearance diff --git a/packages/ui-components/src/components/TabBar/TabBar.stories.tsx b/packages/ui-components/src/components/TabBar/TabBar.stories.tsx index 00138ea7d9..730f9e30db 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.stories.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.stories.tsx @@ -19,7 +19,7 @@ const meta: Meta = { control: false, }, appearance: { - options: ["main", "tabs", "buttons"], + options: ["tabs", "buttons", "content"], control: { type: "radio" }, }, tabStyle: { @@ -113,7 +113,7 @@ export const MainAppearance: Story = { docs: { description: { story: - 'Use `appearance="main"` for the primary navigation as the first child in an AppShell. Each item carries its own bottom border, highlighted on the active item.', + '**Deprecated.** Use `appearance="tabs"` instead. `appearance="main"` keeps working but will be removed in a future major release.', }, }, }, @@ -133,7 +133,7 @@ export const TabsAppearance: Story = { docs: { description: { story: - 'Use `appearance="tabs"` for a TabBar inside page content. Inactive items show a darkened bottom border to visually distinguish them from the active item.', + 'Use `appearance="tabs"` as the default appearance for the primary navigation bar, e.g. as the first child in an AppShell. Each item carries its own bottom border, highlighted on the active item.', }, }, }, @@ -167,3 +167,23 @@ export const ButtonsAppearance: Story = { ], }, } + +export const ContentAppearance: Story = { + parameters: { + docs: { + description: { + story: + 'Use `appearance="content"` for tabs inside page content. Inactive items share a darkened bottom border to visually distinguish them from the active item.', + }, + }, + }, + args: { + appearance: "content", + children: [ + , + , + , + , + ], + }, +} diff --git a/packages/ui-components/src/components/TabBar/TabBar.test.tsx b/packages/ui-components/src/components/TabBar/TabBar.test.tsx index d4348c8091..8dee29dbd1 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.test.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.test.tsx @@ -166,7 +166,7 @@ describe("TabBar", () => { expect(mockOnActiveItemChange).toHaveBeenCalled() }) - test("renders main appearance by default", async () => { + test("renders tabs appearance by default", async () => { await waitFor(() => render( @@ -174,7 +174,7 @@ describe("TabBar", () => { ) ) - expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-main") + expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-tabs") }) test("renders main appearance as passed via appearance prop", async () => { @@ -210,9 +210,7 @@ describe("TabBar", () => { expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-buttons") }) - // Can be removed when the deprecated "content" appearance is removed: - test("still renders the deprecated content appearance as passed via appearance prop", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + test("renders content appearance as passed via appearance prop", async () => { await waitFor(() => render( @@ -221,20 +219,33 @@ describe("TabBar", () => { ) ) expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-content") + }) + + // Can be removed when the deprecated "main" appearance is removed: + test("still renders the deprecated main appearance as passed via appearance prop", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + await waitFor(() => + render( + + + + ) + ) + expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-main") warnSpy.mockRestore() }) - // Can be removed when the deprecated "content" appearance is removed: - test("emits a deprecation warning when the content appearance is used", async () => { + // Can be removed when the deprecated "main" appearance is removed: + test("emits a deprecation warning when the main appearance is used", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) await waitFor(() => render( - + ) ) - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('appearance="content" is deprecated')) + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('appearance="main" is deprecated')) warnSpy.mockRestore() }) diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx index 68a8285bb2..33c1dc2c2f 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx @@ -100,7 +100,7 @@ export const TabBarItem = ({ }: TabBarItemProps): ReactNode => { const { appearance } = useContext(TabBarContext) || {} const isButtons = appearance === "buttons" - const isTabsLike = appearance === "tabs" || appearance === "content" // "content" is the deprecated alias of "tabs" + const isTabsLike = appearance === "content" // "content" is a distinct style variant with a shared bottom border return ( { expect(mockOnClick).toHaveBeenCalled() }) - test("renders main appearance items by default inside a TabBar", () => { + test("renders tabs appearance items by default inside a TabBar", () => { render( ) - expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-main-item") + expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-tabs-item") }) test("renders main appearance items as passed to the parent TabBar via appearance prop", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) render( ) expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-main-item") + warnSpy.mockRestore() }) test("renders tabs appearance items as passed to the parent TabBar via appearance prop", () => { @@ -114,15 +116,24 @@ describe("TabBarItem", () => { expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-buttons-item") }) - // Can be removed when the deprecated "content" appearance is removed: - test("still renders the deprecated content appearance items via appearance prop", () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + test("renders content appearance items as passed to the parent TabBar via appearance prop", () => { render( ) expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-content-item") + }) + + // Can be removed when the deprecated "main" appearance is removed: + test("still renders the deprecated main appearance items via appearance prop", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) + render( + + + + ) + expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass("juno-tabbar-main-item") warnSpy.mockRestore() }) From 70b0bffe7021d44f58ac9be9591debd00ccfe557 Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Mon, 12 Oct 2026 10:34:36 +0200 Subject: [PATCH 3/8] refactor(ui): rename warnedDeprecatedContent ref to warnedDeprecatedMain Signed-off-by: MartinS-git --- .../src/components/TabBar/TabBar.component.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui-components/src/components/TabBar/TabBar.component.tsx b/packages/ui-components/src/components/TabBar/TabBar.component.tsx index 0dfa4b1483..44f54db269 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.component.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.component.tsx @@ -57,10 +57,10 @@ export const TabBar = ({ }: TabBarProps): ReactNode => { const resolvedAppearance = appearance || tabStyle || "tabs" // `|| tabStyle` can be removed when TabNavigation is removed - const warnedDeprecatedContent = useRef(false) + const warnedDeprecatedMain = useRef(false) useEffect(() => { - if (resolvedAppearance === "main" && !warnedDeprecatedContent.current) { - warnedDeprecatedContent.current = true + if (resolvedAppearance === "main" && !warnedDeprecatedMain.current) { + warnedDeprecatedMain.current = true console.warn('TabBar: appearance="main" is deprecated. Use appearance="tabs" instead.') } }, [resolvedAppearance]) From 14d9200373b5d460f87343922b65a7ce2c2cef4d Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Mon, 12 Oct 2026 11:07:16 +0200 Subject: [PATCH 4/8] fix(ui): prevent hover style on disabled TabBar button items Signed-off-by: MartinS-git --- .../src/components/TabBarItem/TabBarItem.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx index 33c1dc2c2f..9dacadbf4b 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx @@ -54,7 +54,7 @@ const tabBarItemButtonActiveStyles = ` const tabBarItemButtonInactiveStyles = ` jn:text-theme-tab-button jn:border-transparent - jn:hover:text-theme-tab-button-hover + jn:not-[[aria-disabled=true]]:hover:text-theme-tab-button-hover ` const tabBarItemTabsStyles = ` From 057f1801fdf179ffe0eec0503cda539ae2cec96c Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Tue, 13 Oct 2026 09:45:36 +0200 Subject: [PATCH 5/8] fix(ui): address TabBar review feedback - Fix double warning: TabNavigation no longer triggers the appearance="main" deprecation warning. useEffect now checks the raw appearance prop. - Rename internal *Tabs* variables to *Content* to match the appearance they style. - Add jn:w-fit to buttons appearance so it no longer spans full container width. - Reorder appearance control options in stories: tabs -> content -> buttons. Signed-off-by: MartinS-git --- .../src/components/TabBar/TabBar.component.tsx | 5 +++-- .../src/components/TabBar/TabBar.stories.tsx | 2 +- .../TabBarItem/TabBarItem.component.tsx | 16 ++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/ui-components/src/components/TabBar/TabBar.component.tsx b/packages/ui-components/src/components/TabBar/TabBar.component.tsx index 44f54db269..8e390ec47a 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.component.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.component.tsx @@ -11,6 +11,7 @@ const tabBarStyles = ` ` const tabBarButtonsStyles = ` + jn:w-fit jn:bg-theme-tab-buttons jn:rounded-[0.25rem] jn:p-[0.125rem] @@ -59,11 +60,11 @@ export const TabBar = ({ const warnedDeprecatedMain = useRef(false) useEffect(() => { - if (resolvedAppearance === "main" && !warnedDeprecatedMain.current) { + if (appearance === "main" && !warnedDeprecatedMain.current) { warnedDeprecatedMain.current = true console.warn('TabBar: appearance="main" is deprecated. Use appearance="tabs" instead.') } - }, [resolvedAppearance]) + }, [appearance]) return ( = { control: false, }, appearance: { - options: ["tabs", "buttons", "content"], + options: ["tabs", "content", "buttons"], control: { type: "radio" }, }, tabStyle: { diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx index 9dacadbf4b..815741ae95 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx @@ -57,7 +57,7 @@ const tabBarItemButtonInactiveStyles = ` jn:not-[[aria-disabled=true]]:hover:text-theme-tab-button-hover ` -const tabBarItemTabsStyles = ` +const tabBarItemContentStyles = ` jn:flex jn:items-center jn:text-theme-default @@ -69,12 +69,12 @@ const tabBarItemTabsStyles = ` jn:focus-visible:ring-theme-focus ` -const tabBarActiveTabsItemStyles = ` +const tabBarActiveContentItemStyles = ` jn:text-theme-high jn:font-bold ` -const tabBarItemTabsWrapperStyles = ` +const tabBarItemContentWrapperStyles = ` jn:border-b-[3px] jn:border-theme-tab-content-inactive-bottom jn:has-[.juno-navigation-item-active]:border-theme-tab-active-bottom @@ -100,28 +100,28 @@ export const TabBarItem = ({ }: TabBarItemProps): ReactNode => { const { appearance } = useContext(TabBarContext) || {} const isButtons = appearance === "buttons" - const isTabsLike = appearance === "content" // "content" is a distinct style variant with a shared bottom border + const isContentLike = appearance === "content" // "content" is a distinct style variant with a shared bottom border return ( {children} From d65358ff3606424864745e603e4c0f9a231fb84e Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Wed, 14 Oct 2026 09:50:33 +0200 Subject: [PATCH 6/8] style(ui): apply prettier formatting to TabBarItem Signed-off-by: MartinS-git --- .../src/components/TabBarItem/TabBarItem.component.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx index 815741ae95..378f0feb2f 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx @@ -105,7 +105,11 @@ export const TabBarItem = ({ Date: Wed, 14 Oct 2026 10:49:54 +0200 Subject: [PATCH 7/8] test(ui): remove redundant deprecated main appearance test in TabBar Signed-off-by: MartinS-git --- .../src/components/TabBar/TabBar.test.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/packages/ui-components/src/components/TabBar/TabBar.test.tsx b/packages/ui-components/src/components/TabBar/TabBar.test.tsx index 8dee29dbd1..d1542aebba 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.test.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.test.tsx @@ -221,20 +221,6 @@ describe("TabBar", () => { expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-content") }) - // Can be removed when the deprecated "main" appearance is removed: - test("still renders the deprecated main appearance as passed via appearance prop", async () => { - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) - await waitFor(() => - render( - - - - ) - ) - expect(screen.getByRole("navigation")).toHaveClass("juno-tabbar-main") - warnSpy.mockRestore() - }) - // Can be removed when the deprecated "main" appearance is removed: test("emits a deprecation warning when the main appearance is used", async () => { const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) From 884a11500e2666ed08f4357ed37c67563f0e117c Mon Sep 17 00:00:00 2001 From: MartinS-git Date: Wed, 14 Oct 2026 12:03:02 +0200 Subject: [PATCH 8/8] refactor(ui): replace arbitrary Tailwind values with standard utilities in TabBar Swap bracket-value classes for standard Tailwind utilities with identical computed CSS: rounded-sm, p-0.5, py-3.5, px-6.25, px-2.5, py-1.75. Signed-off-by: MartinS-git --- .../src/components/TabBar/TabBar.component.tsx | 4 ++-- .../components/TabBarItem/TabBarItem.component.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/ui-components/src/components/TabBar/TabBar.component.tsx b/packages/ui-components/src/components/TabBar/TabBar.component.tsx index 8e390ec47a..7d5f1ef29d 100644 --- a/packages/ui-components/src/components/TabBar/TabBar.component.tsx +++ b/packages/ui-components/src/components/TabBar/TabBar.component.tsx @@ -13,8 +13,8 @@ const tabBarStyles = ` const tabBarButtonsStyles = ` jn:w-fit jn:bg-theme-tab-buttons - jn:rounded-[0.25rem] - jn:p-[0.125rem] + jn:rounded-sm + jn:p-0.5 ` /** @deprecated Use `"tabs"` instead. `"main"` remains functional but will be removed in a future major release. */ diff --git a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx index 378f0feb2f..7a2873129c 100644 --- a/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx +++ b/packages/ui-components/src/components/TabBarItem/TabBarItem.component.tsx @@ -13,8 +13,8 @@ const tabBarItemStyles = ` jn:items-center jn:text-theme-default jn:font-bold - jn:py-[0.875rem] - jn:px-[1.5625rem] + jn:py-3.5 + jn:px-6.25 jn:border-b-[3px] jn:focus-visible:outline-hidden jn:focus-visible:ring-2 @@ -30,8 +30,8 @@ const tabBarActiveItemStyles = ` const tabBarItemButtonStyles = ` jn:rounded-[0.1875rem] - jn:px-[0.625rem] - jn:py-[0.4375rem] + jn:px-2.5 + jn:py-1.75 jn:text-sm jn:font-bold jn:leading-[1.4] @@ -62,8 +62,8 @@ const tabBarItemContentStyles = ` jn:items-center jn:text-theme-default jn:font-bold - jn:py-[0.875rem] - jn:px-[1.5625rem] + jn:py-3.5 + jn:px-6.25 jn:focus-visible:outline-hidden jn:focus-visible:ring-2 jn:focus-visible:ring-theme-focus