From 9892f76eec6dee718848c00808d65c53392e21b1 Mon Sep 17 00:00:00 2001 From: ux-git Date: Fri, 4 Sep 2026 03:04:01 +0200 Subject: [PATCH 1/7] fix(components): unify the tab styling and stop the selected tab from resizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tabs picked up their look in five different places: every page that wanted flat tabs copied the same block of overrides onto the shared component, and the copies disagreed — some cleared the ripple radius, others rounded it to 8px, while the tabs underneath stayed fully rounded. Selecting a tab also swapped its typography class, and the heavier text measured wider, so the neighbouring tabs shifted. The two looks are now one prop on the shared component. `chip` keeps the fully rounded pill the date and period selectors use, `plain` gives the page section tabs a 4px background on hover and while pressed, and both come from a single style module. The five copies of the override block are gone. The label renders both typographic states stacked in one grid cell, so a tab always reserves the width of its selected state and nothing moves when the weight changes. It measures whatever the label happens to be, so translations of any length keep working without a fixed width. --- src/components/scrollable_tabs/index.tsx | 38 +++++-------------- src/components/tabs/index.styles.ts | 33 ++++++++++++++++ src/components/tabs/index.tsx | 20 +++++----- src/components/tabs/index.types.ts | 12 ++++++ src/components/tabs/tab_label/index.tsx | 29 ++++++++++++++ src/components/tabs/tab_label/index.types.ts | 6 +++ src/features/persons/applications/index.tsx | 9 +---- .../my_congregation/index.tsx | 9 +---- .../export_S21/specific_records/index.tsx | 14 +------ .../publisher_tabs/index.tsx | 14 +------ src/pages/meetings/schedules/index.tsx | 9 +---- 11 files changed, 105 insertions(+), 88 deletions(-) create mode 100644 src/components/tabs/index.styles.ts create mode 100644 src/components/tabs/tab_label/index.tsx create mode 100644 src/components/tabs/tab_label/index.types.ts diff --git a/src/components/scrollable_tabs/index.tsx b/src/components/scrollable_tabs/index.tsx index d43c41dca6c..137a86d2cfa 100644 --- a/src/components/scrollable_tabs/index.tsx +++ b/src/components/scrollable_tabs/index.tsx @@ -9,6 +9,8 @@ import { Box, Tab, Tabs, tabsClasses } from '@mui/material'; import { useBreakpoints } from '@hooks/index'; import { CustomTabPanel } from '@components/tabs'; import { CustomTabProps } from '@components/tabs/index.types'; +import { tabsSharedStyles } from '@components/tabs/index.styles'; +import TabLabel from '@components/tabs/tab_label'; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import Typography from '@components/typography'; @@ -26,6 +28,7 @@ function ScrollableTabs({ variant = 'scrollable', minHeight = '48px', tabsCountOnScreen = 0, + appearance = 'chip', sx, }: CustomTabProps) { const { tabletDown } = useBreakpoints(); @@ -89,26 +92,7 @@ function ScrollableTabs({ color: 'var(--accent-main)', '&.Mui-disabled': { opacity: 0.3 }, }, - '& button.Mui-selected': { - color: 'var(--accent-main)', - background: 'var(--accent-150)', - borderRadius: 'var(--radius-max)', - }, - '& .MuiTab-root:not(.Mui-selected)': { - color: 'var(--grey-350)', - '&:hover': { - background: - 'color-mix(in srgb, var(--accent-150) 38%, transparent)', - borderRadius: 'var(--radius-max)', - }, - }, - // Programatically changing color of ripple (wave) when click happens: - '& span.MuiTouchRipple-rippleVisible': { - color: 'var(--accent-main)', - }, - '& span.MuiTouchRipple-root': { - borderRadius: 'var(--radius-max)', - }, + ...tabsSharedStyles(appearance), '& .MuiSvgIcon-root g path': { fill: 'var(--accent-400)', }, @@ -130,7 +114,12 @@ function ScrollableTabs({ {tabs.map( ({ label, icon, className }, index): ReactNode => ( + } key={index} className={className} icon={icon} @@ -143,13 +132,6 @@ function ScrollableTabs({ ? `calc(100% / ${tabsCountOnScreen})` : 'auto', minWidth: '20px', - fontSize: 16, - textTransform: 'none', - ':not(&.Mui-selected)': { fontWeight: 400 }, - '&.Mui-Selected': { - fontWeight: 600, - fontSize: 18, - }, }} /> ) diff --git a/src/components/tabs/index.styles.ts b/src/components/tabs/index.styles.ts new file mode 100644 index 00000000000..9efb962b99c --- /dev/null +++ b/src/components/tabs/index.styles.ts @@ -0,0 +1,33 @@ +import { SxProps, Theme } from '@mui/material'; +import { TabsAppearance } from './index.types'; + +export const tabsSharedStyles = ( + appearance: TabsAppearance = 'chip' +): SxProps => { + const radius = + appearance === 'chip' ? 'var(--radius-max)' : 'var(--radius-s)'; + + return { + '& .MuiTab-root': { + borderRadius: radius, + textTransform: 'none', + transition: 'background-color 0.2s', + }, + '& .MuiTab-root:hover': { + backgroundColor: 'color-mix(in srgb, var(--accent-150) 38%, transparent)', + }, + '& button.Mui-selected': { + color: 'var(--accent-main)', + background: appearance === 'chip' ? 'var(--accent-150)' : 'unset', + }, + '& button:not(.Mui-selected)': { + color: 'var(--grey-350)', + }, + '& .MuiTouchRipple-root': { + borderRadius: radius, + }, + '& span.MuiTouchRipple-rippleVisible': { + color: 'var(--accent-main)', + }, + }; +}; diff --git a/src/components/tabs/index.tsx b/src/components/tabs/index.tsx index 1fab0c6666b..1d71c750edd 100644 --- a/src/components/tabs/index.tsx +++ b/src/components/tabs/index.tsx @@ -1,6 +1,8 @@ import { ReactNode, SyntheticEvent, useEffect, useState } from 'react'; import { Tabs as MUITabs, Tab, Box } from '@mui/material'; import { TabsPanelProps, CustomTabProps } from './index.types'; +import { tabsSharedStyles } from './index.styles'; +import TabLabel from './tab_label'; import useBreakpoints from '@hooks/useBreakpoints'; /** @@ -94,21 +96,19 @@ const Tabs = ({ }, }, }} - sx={{ - '& button.Mui-selected': { color: 'var(--accent-main)' }, - '& button:not(.Mui-selected)': { color: 'var(--grey-350)' }, - // Programatically changing color of ripple (wave) when click happens: - '& span.MuiTouchRipple-rippleVisible': { - color: 'var(--accent-main)', - }, - }} + sx={tabsSharedStyles('plain')} > {tabs.map( ({ label, className }, index): ReactNode => ( + } key={index} - className={`${valueOfActivePanel === index ? 'h4' : 'body-regular'} ${className}`} + className={className} {...a11yProps(index)} /> ) diff --git a/src/components/tabs/index.types.ts b/src/components/tabs/index.types.ts index ec1ae8c5438..9305039675f 100644 --- a/src/components/tabs/index.types.ts +++ b/src/components/tabs/index.types.ts @@ -21,10 +21,22 @@ export type TabsPanelProps = { value: number | boolean; }; +/** + * The look of the tabs. + * - `chip` - fully rounded tabs, used for the date and period selectors. + * - `plain` - lightly rounded tabs, used wherever tabs switch a page section. + */ +export type TabsAppearance = 'chip' | 'plain'; + /** * Custom props for the Tab component. */ export interface CustomTabProps extends TabOwnProps { + /** + * The look of the tabs. (Default: chip) + */ + appearance?: TabsAppearance; + /** * An array of tabs containing label and corresponding component. */ diff --git a/src/components/tabs/tab_label/index.tsx b/src/components/tabs/tab_label/index.tsx new file mode 100644 index 00000000000..00332d91bbf --- /dev/null +++ b/src/components/tabs/tab_label/index.tsx @@ -0,0 +1,29 @@ +import { Box } from '@mui/material'; +import { TabLabelProps } from './index.types'; +import Typography from '@components/typography'; + +const TabLabel = ({ label, selected }: TabLabelProps) => { + return ( + *': { gridArea: '1 / 1' } }}> + + {label} + + + + {label} + + + ); +}; + +export default TabLabel; diff --git a/src/components/tabs/tab_label/index.types.ts b/src/components/tabs/tab_label/index.types.ts new file mode 100644 index 00000000000..cb9004aae2e --- /dev/null +++ b/src/components/tabs/tab_label/index.types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export type TabLabelProps = { + label: ReactNode; + selected: boolean; +}; diff --git a/src/features/persons/applications/index.tsx b/src/features/persons/applications/index.tsx index 6f4002b3792..e986ee7409e 100644 --- a/src/features/persons/applications/index.tsx +++ b/src/features/persons/applications/index.tsx @@ -32,18 +32,11 @@ const PersonApplications = () => { diff --git a/src/features/persons/speakers_catalog/my_congregation/index.tsx b/src/features/persons/speakers_catalog/my_congregation/index.tsx index 8650734d0a4..6f296e7fa78 100644 --- a/src/features/persons/speakers_catalog/my_congregation/index.tsx +++ b/src/features/persons/speakers_catalog/my_congregation/index.tsx @@ -45,17 +45,10 @@ const MyCongregation = () => { diff --git a/src/features/reports/publisher_records/export_S21/specific_records/index.tsx b/src/features/reports/publisher_records/export_S21/specific_records/index.tsx index 9306f689c7f..e7adc3d5f8d 100644 --- a/src/features/reports/publisher_records/export_S21/specific_records/index.tsx +++ b/src/features/reports/publisher_records/export_S21/specific_records/index.tsx @@ -20,19 +20,7 @@ const SpecificRecords = (props: SpecificRecordsProps) => { - + ); }; diff --git a/src/features/reports/publisher_records/publisher_tabs/index.tsx b/src/features/reports/publisher_records/publisher_tabs/index.tsx index 04dedccb4a9..d30969911ca 100644 --- a/src/features/reports/publisher_records/publisher_tabs/index.tsx +++ b/src/features/reports/publisher_records/publisher_tabs/index.tsx @@ -7,19 +7,7 @@ const PublisherTabs = () => { return ( - + ); }; diff --git a/src/pages/meetings/schedules/index.tsx b/src/pages/meetings/schedules/index.tsx index 5ced84ffebe..6c89a84172f 100644 --- a/src/pages/meetings/schedules/index.tsx +++ b/src/pages/meetings/schedules/index.tsx @@ -31,17 +31,10 @@ const WeeklySchedules = () => { > From 8ecdf9eb3061e55220d19c5b050daf8d71d81a2d Mon Sep 17 00:00:00 2001 From: ux-git Date: Fri, 4 Sep 2026 03:04:02 +0200 Subject: [PATCH 2/7] fix(components): center the tab label and add a badge option to the tabs The badge next to a tab label was passed in as a custom node built by each call site, which nested block elements inside a typography and shifted the label 12px to the right whenever the count was zero. The badge is now an option of the shared tab label, so every tab renders it the same way. --- src/components/scrollable_tabs/index.tsx | 3 +- src/components/tab_label_with_badge/index.tsx | 4 -- src/components/tabs/index.tsx | 3 +- src/components/tabs/index.types.ts | 5 ++ src/components/tabs/tab_label/index.tsx | 66 ++++++++++++++----- src/components/tabs/tab_label/index.types.ts | 15 +++++ .../meetings/my_assignments/index.tsx | 12 ++-- src/features/persons/filter/index.tsx | 14 ++-- .../publisher_tabs/usePublisherTabs.tsx | 17 ++--- 9 files changed, 85 insertions(+), 54 deletions(-) diff --git a/src/components/scrollable_tabs/index.tsx b/src/components/scrollable_tabs/index.tsx index 137a86d2cfa..789103e942d 100644 --- a/src/components/scrollable_tabs/index.tsx +++ b/src/components/scrollable_tabs/index.tsx @@ -112,11 +112,12 @@ function ScrollableTabs({ }} > {tabs.map( - ({ label, icon, className }, index): ReactNode => ( + ({ label, badge, icon, className }, index): ReactNode => ( } diff --git a/src/components/tab_label_with_badge/index.tsx b/src/components/tab_label_with_badge/index.tsx index 6d53658f2d8..d0172c28722 100644 --- a/src/components/tab_label_with_badge/index.tsx +++ b/src/components/tab_label_with_badge/index.tsx @@ -19,8 +19,6 @@ const LabelBadge = ({ alignItems: 'center', height: '24px', fontSize: '14px', - opacity: 1, - transition: 'opacity 0.2s', }} > @@ -48,8 +46,6 @@ const TabLabelWithBadge = ({ display: 'flex', alignItems: 'center', gap: '8px', - transform: count === 0 && 'translateX(12px)', - transition: 'transform 0.2s', userSelect: 'none', }} > diff --git a/src/components/tabs/index.tsx b/src/components/tabs/index.tsx index 1d71c750edd..7e66347ea7e 100644 --- a/src/components/tabs/index.tsx +++ b/src/components/tabs/index.tsx @@ -99,11 +99,12 @@ const Tabs = ({ sx={tabsSharedStyles('plain')} > {tabs.map( - ({ label, className }, index): ReactNode => ( + ({ label, badge, className }, index): ReactNode => ( } diff --git a/src/components/tabs/index.types.ts b/src/components/tabs/index.types.ts index 9305039675f..26c177b6055 100644 --- a/src/components/tabs/index.types.ts +++ b/src/components/tabs/index.types.ts @@ -46,6 +46,11 @@ export interface CustomTabProps extends TabOwnProps { */ label: string | ReactNode; + /** + * The number displayed in a badge next to the label. + */ + badge?: number; + /** * The component to be rendered in the tab. */ diff --git a/src/components/tabs/tab_label/index.tsx b/src/components/tabs/tab_label/index.tsx index 00332d91bbf..114c502066f 100644 --- a/src/components/tabs/tab_label/index.tsx +++ b/src/components/tabs/tab_label/index.tsx @@ -2,26 +2,58 @@ import { Box } from '@mui/material'; import { TabLabelProps } from './index.types'; import Typography from '@components/typography'; -const TabLabel = ({ label, selected }: TabLabelProps) => { +/** + * Renders the content of a tab. + * + * Both typographies are stacked in the same grid cell so that the tab keeps + * the width of its bold state and the neighboring tabs never shift. + * + * @param props The props for the TabLabel component. + */ +const TabLabel = ({ label, badge, selected }: TabLabelProps) => { return ( - *': { gridArea: '1 / 1' } }}> - + *': { gridArea: '1 / 1' }, + }} > - {label} - + + {label} + - - {label} - + + {label} + + + + {typeof badge === 'number' && ( + + + {badge} + + + )} ); }; diff --git a/src/components/tabs/tab_label/index.types.ts b/src/components/tabs/tab_label/index.types.ts index cb9004aae2e..b4da56a4b73 100644 --- a/src/components/tabs/tab_label/index.types.ts +++ b/src/components/tabs/tab_label/index.types.ts @@ -1,6 +1,21 @@ import { ReactNode } from 'react'; +/** + * Props for the TabLabel component. + */ export type TabLabelProps = { + /** + * The label of the tab. + */ label: ReactNode; + + /** + * The number displayed in a badge next to the label. + */ + badge?: number; + + /** + * A boolean indicating whether the tab is the selected one. + */ selected: boolean; }; diff --git a/src/features/meetings/my_assignments/index.tsx b/src/features/meetings/my_assignments/index.tsx index 769bd5108fe..d7e8226ee39 100644 --- a/src/features/meetings/my_assignments/index.tsx +++ b/src/features/meetings/my_assignments/index.tsx @@ -10,7 +10,6 @@ import MenuItem from '@components/menuitem'; import MonthContainer from './month_container'; import NoAssigmentsImg from '@assets/img/illustration_no_assigments.svg?component'; import Select from '@components/select'; -import TabLabel from '@components/tab_label_with_badge'; import Tabs from '@components/tabs'; import Typography from '@components/typography'; @@ -103,18 +102,15 @@ const MyAssignments = () => { const tabs = [ { - label: , + label: t('tr_myOwn'), + badge: ownAssignments.total, Component: renderAssignments(ownAssignments.byDate), }, ...(hasDelegatedAssignments ? [ { - label: ( - - ), + label: t('tr_delegated'), + badge: delegateAssignments.total, Component: renderAssignments(delegateAssignments.byDate), }, ] diff --git a/src/features/persons/filter/index.tsx b/src/features/persons/filter/index.tsx index cac3d646828..cf67a4e55f4 100644 --- a/src/features/persons/filter/index.tsx +++ b/src/features/persons/filter/index.tsx @@ -6,7 +6,6 @@ import { useAppTranslation, useBreakpoints } from '@hooks/index'; import useFilter from './useFilter'; import AssignmentGroup from '../assignment_group'; import Tabs from '@components/tabs'; -import TabLabel from '@components/tab_label_with_badge'; const PersonsFilter = () => { const { t } = useAppTranslation(); @@ -26,12 +25,8 @@ const PersonsFilter = () => { const tabs = [ { - label: ( - - ), + label: t('tr_categories'), + badge: filters.length - checkedItems.length, Component: ( { ), }, { - label: ( - - ), + label: t('tr_assignments'), + badge: checkedItems.length, Component: ( { const { t } = useAppTranslation(); @@ -42,21 +41,13 @@ const usePublisherTabs = () => { const tabs = useMemo(() => { return [ { - label: ( - - ), + label: t('tr_activePublishers'), + badge: publishers.active, Component: , }, { - label: ( - - ), + label: t('tr_inactivePublishers'), + badge: publishers.inactive, Component: , }, ]; From 13d808db99d24a920b0fc7b24667e35a784e008c Mon Sep 17 00:00:00 2001 From: ux-git Date: Fri, 4 Sep 2026 03:04:02 +0200 Subject: [PATCH 3/7] refactor(components): let the tabs own their width behavior Three pages repeated the same breakpoint check to pick the MUI tab variant. They now state what they want with a layout prop and the component resolves the breakpoint, next to the unused tabs count prop that has been dropped. --- src/components/scrollable_tabs/index.tsx | 11 ++++------ src/components/tabs/index.types.ts | 21 ++++++++++--------- .../monthly_history/year_selector/index.tsx | 5 +---- .../publisher_records/years_stats/index.tsx | 10 ++------- .../years_stats/index.tsx | 10 ++------- 5 files changed, 20 insertions(+), 37 deletions(-) diff --git a/src/components/scrollable_tabs/index.tsx b/src/components/scrollable_tabs/index.tsx index 789103e942d..4d355f4fe2c 100644 --- a/src/components/scrollable_tabs/index.tsx +++ b/src/components/scrollable_tabs/index.tsx @@ -25,13 +25,14 @@ function ScrollableTabs({ indicatorMode, onChange, className, - variant = 'scrollable', minHeight = '48px', - tabsCountOnScreen = 0, + layout = 'auto', appearance = 'chip', sx, }: CustomTabProps) { - const { tabletDown } = useBreakpoints(); + const { tabletDown, tabletUp } = useBreakpoints(); + + const variant = layout === 'stretch' && tabletUp ? 'fullWidth' : 'scrollable'; const [valueOfActivePanel, setValueOfActivePanel] = useState(value ?? false); @@ -128,10 +129,6 @@ function ScrollableTabs({ sx={{ minHeight, height: minHeight, - width: - tabsCountOnScreen !== 0 - ? `calc(100% / ${tabsCountOnScreen})` - : 'auto', minWidth: '20px', }} /> diff --git a/src/components/tabs/index.types.ts b/src/components/tabs/index.types.ts index 26c177b6055..b6c79167051 100644 --- a/src/components/tabs/index.types.ts +++ b/src/components/tabs/index.types.ts @@ -1,5 +1,5 @@ import { ReactElement, ReactNode } from 'react'; -import { SxProps, TabOwnProps, TabsOwnProps, Theme } from '@mui/material'; +import { SxProps, TabOwnProps, Theme } from '@mui/material'; /** * Props for the TabsPanel component. @@ -28,6 +28,14 @@ export type TabsPanelProps = { */ export type TabsAppearance = 'chip' | 'plain'; +/** + * The width of the tabs. + * - `auto` - every tab takes the width of its own label. + * - `stretch` - the tabs share the full width from the tablet breakpoint up, + * and fall back to `auto` on smaller screens. + */ +export type TabsLayout = 'auto' | 'stretch'; + /** * Custom props for the Tab component. */ @@ -88,10 +96,9 @@ export interface CustomTabProps extends TabOwnProps { className?: string; /** - * The variant of the tabs, aligning with the MUI `TabsOwnProps` variant. - * Examples include `"scrollable"` or `"standard"`. + * The width of the tabs. (Default: auto) */ - variant?: TabsOwnProps['variant']; + layout?: TabsLayout; /** * Minimum height for the tab component. @@ -99,12 +106,6 @@ export interface CustomTabProps extends TabOwnProps { */ minHeight?: string; - /** - * The number of tabs that should be displayed on the screen at once. - * Useful for responsive layouts or custom tab implementations. - */ - tabsCountOnScreen?: number; - /** * The action component to be displayed with the tab. */ diff --git a/src/features/reports/meeting_attendance/monthly_history/year_selector/index.tsx b/src/features/reports/meeting_attendance/monthly_history/year_selector/index.tsx index 87c92fb5056..0ebdc821ca6 100644 --- a/src/features/reports/meeting_attendance/monthly_history/year_selector/index.tsx +++ b/src/features/reports/meeting_attendance/monthly_history/year_selector/index.tsx @@ -1,5 +1,4 @@ import { Box } from '@mui/material'; -import { useBreakpoints } from '@hooks/index'; import { YearSelectorProps } from './index.types'; import useYearSelector from './useYearSelector'; import ScrollableTabs from '@components/scrollable_tabs'; @@ -7,12 +6,10 @@ import ScrollableTabs from '@components/scrollable_tabs'; const YearSelector = ({ onChange, value }: YearSelectorProps) => { const { tabs } = useYearSelector(); - const { tabletUp } = useBreakpoints(); - return ( { const { t } = useAppTranslation(); - const { tabletUp } = useBreakpoints(); - const { tabs, intial_value } = useYearsStats(); return ( {t('tr_statistics')} - + ); }; diff --git a/src/features/reports/publisher_records_details/years_stats/index.tsx b/src/features/reports/publisher_records_details/years_stats/index.tsx index c2c83606c5a..4bc93ec0f3e 100644 --- a/src/features/reports/publisher_records_details/years_stats/index.tsx +++ b/src/features/reports/publisher_records_details/years_stats/index.tsx @@ -1,4 +1,4 @@ -import { useAppTranslation, useBreakpoints } from '@hooks/index'; +import { useAppTranslation } from '@hooks/index'; import useYearsStats from './useYearsStats'; import Card from '@components/card'; import ScrollableTabs from '@components/scrollable_tabs'; @@ -7,19 +7,13 @@ import Typography from '@components/typography'; const YearsStats = () => { const { t } = useAppTranslation(); - const { tabletUp } = useBreakpoints(); - const { tabs, intial_value } = useYearsStats(); return ( {t('tr_serviceYear')} - + ); }; From 309f6d5e01daf4fb028438f8521b664029e6c73e Mon Sep 17 00:00:00 2001 From: ux-git Date: Fri, 4 Sep 2026 03:04:02 +0200 Subject: [PATCH 4/7] refactor(components): drop the tab props that repeat another one The indicator was requested by every plain tab row and by none of the chip ones, so it now follows the appearance instead of being asked for twice, and the tab height that no page ever set became a constant. The segmented switcher reads its font from the global classes rather than repeating them. --- src/components/scrollable_tabs/index.tsx | 20 ++++++++++--------- src/components/tab_switcher/index.tsx | 4 +--- src/components/tabs/index.types.ts | 11 ---------- src/components/tabs/tab_label/index.tsx | 5 +---- src/features/persons/applications/index.tsx | 1 - .../my_congregation/index.tsx | 7 +------ .../export_S21/specific_records/index.tsx | 2 +- .../publisher_tabs/index.tsx | 2 +- src/pages/meetings/schedules/index.tsx | 1 - 9 files changed, 16 insertions(+), 37 deletions(-) diff --git a/src/components/scrollable_tabs/index.tsx b/src/components/scrollable_tabs/index.tsx index 4d355f4fe2c..512791a41af 100644 --- a/src/components/scrollable_tabs/index.tsx +++ b/src/components/scrollable_tabs/index.tsx @@ -15,6 +15,8 @@ import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import Typography from '@components/typography'; +const TAB_HEIGHT = '48px'; + /** * Component that renders scrollable tabs. * @@ -22,10 +24,8 @@ import Typography from '@components/typography'; function ScrollableTabs({ tabs, value, - indicatorMode, onChange, className, - minHeight = '48px', layout = 'auto', appearance = 'chip', sx, @@ -34,6 +34,8 @@ function ScrollableTabs({ const variant = layout === 'stretch' && tabletUp ? 'fullWidth' : 'scrollable'; + const hasIndicator = appearance === 'plain'; + const [valueOfActivePanel, setValueOfActivePanel] = useState(value ?? false); /** @@ -43,7 +45,7 @@ function ScrollableTabs({ * @param newValue The new value of the active tab. */ const handleChange = (event: SyntheticEvent, newValue: number) => { - if (!indicatorMode) { + if (!hasIndicator) { event.preventDefault(); } @@ -73,12 +75,12 @@ function ScrollableTabs({ className={className} slotProps={{ indicator: { - hidden: !indicatorMode, + hidden: !hasIndicator, sx: { - backgroundColor: indicatorMode + backgroundColor: hasIndicator ? 'var(--accent-main)' : 'transparent', - borderRadius: indicatorMode && '16px 16px 0px 0px', + borderRadius: hasIndicator && '16px 16px 0px 0px', height: '4px', }, }, @@ -105,7 +107,7 @@ function ScrollableTabs({ height: '36px', }, alignItems: 'center', - minHeight, + minHeight: TAB_HEIGHT, [`& .${tabsClasses.flexContainer}`]: { gap: '4px', }, @@ -127,8 +129,8 @@ function ScrollableTabs({ icon={icon} iconPosition="end" sx={{ - minHeight, - height: minHeight, + minHeight: TAB_HEIGHT, + height: TAB_HEIGHT, minWidth: '20px', }} /> diff --git a/src/components/tab_switcher/index.tsx b/src/components/tab_switcher/index.tsx index 9d6647af1b2..1d3681df6e3 100644 --- a/src/components/tab_switcher/index.tsx +++ b/src/components/tab_switcher/index.tsx @@ -52,6 +52,7 @@ const TabSwitcher = ({ ({ minHeight: '28px', borderRadius: 'var(--radius-m)', fontFamily: 'inherit', - fontSize: '15px', - lineHeight: '20px', - fontWeight: isActive ? 500 : 400, color: isActive ? 'var(--accent-dark)' : 'var(--accent-400)', transition: 'color 0.16s ease-out', '&.Mui-disabled': { opacity: 0.5 }, diff --git a/src/components/tabs/index.types.ts b/src/components/tabs/index.types.ts index b6c79167051..12f01709922 100644 --- a/src/components/tabs/index.types.ts +++ b/src/components/tabs/index.types.ts @@ -77,11 +77,6 @@ export interface CustomTabProps extends TabOwnProps { */ value?: number | boolean; - /** - * A boolean indicating whether to display the indicator. - */ - indicatorMode?: boolean; - /** * Callback function triggered when the active tab changes. * Provides the new active tab index as a parameter. @@ -100,12 +95,6 @@ export interface CustomTabProps extends TabOwnProps { */ layout?: TabsLayout; - /** - * Minimum height for the tab component. - * Useful for ensuring consistent tab sizes. - */ - minHeight?: string; - /** * The action component to be displayed with the tab. */ diff --git a/src/components/tabs/tab_label/index.tsx b/src/components/tabs/tab_label/index.tsx index 114c502066f..863c540efd4 100644 --- a/src/components/tabs/tab_label/index.tsx +++ b/src/components/tabs/tab_label/index.tsx @@ -3,10 +3,7 @@ import { TabLabelProps } from './index.types'; import Typography from '@components/typography'; /** - * Renders the content of a tab. - * - * Both typographies are stacked in the same grid cell so that the tab keeps - * the width of its bold state and the neighboring tabs never shift. + * A custom tab label component. * * @param props The props for the TabLabel component. */ diff --git a/src/features/persons/applications/index.tsx b/src/features/persons/applications/index.tsx index e986ee7409e..b31ece88665 100644 --- a/src/features/persons/applications/index.tsx +++ b/src/features/persons/applications/index.tsx @@ -33,7 +33,6 @@ const PersonApplications = () => { { > - + diff --git a/src/features/reports/publisher_records/export_S21/specific_records/index.tsx b/src/features/reports/publisher_records/export_S21/specific_records/index.tsx index e7adc3d5f8d..ad15dd79769 100644 --- a/src/features/reports/publisher_records/export_S21/specific_records/index.tsx +++ b/src/features/reports/publisher_records/export_S21/specific_records/index.tsx @@ -20,7 +20,7 @@ const SpecificRecords = (props: SpecificRecordsProps) => { - + ); }; diff --git a/src/features/reports/publisher_records/publisher_tabs/index.tsx b/src/features/reports/publisher_records/publisher_tabs/index.tsx index d30969911ca..9b5924e4ffb 100644 --- a/src/features/reports/publisher_records/publisher_tabs/index.tsx +++ b/src/features/reports/publisher_records/publisher_tabs/index.tsx @@ -7,7 +7,7 @@ const PublisherTabs = () => { return ( - + ); }; diff --git a/src/pages/meetings/schedules/index.tsx b/src/pages/meetings/schedules/index.tsx index 6c89a84172f..8832002b020 100644 --- a/src/pages/meetings/schedules/index.tsx +++ b/src/pages/meetings/schedules/index.tsx @@ -30,7 +30,6 @@ const WeeklySchedules = () => { }} > Date: Fri, 4 Sep 2026 03:48:04 +0200 Subject: [PATCH 5/7] fix(components): let the tabs honour the appearance they are given The component accepted an appearance but always drew the plain one, so a caller asking for chips got plain tabs. It passes the prop through now, and defaults to plain, which is what every caller renders today. --- src/components/tabs/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/index.tsx b/src/components/tabs/index.tsx index 7e66347ea7e..0658f63918c 100644 --- a/src/components/tabs/index.tsx +++ b/src/components/tabs/index.tsx @@ -51,6 +51,8 @@ const Tabs = ({ onChange, actionComponent, showTabs = true, + // the plain look is what every caller renders today; a chip is opt-in + appearance = 'plain', }: CustomTabProps) => { const [valueOfActivePanel, setValueOfActivePanel] = useState(value || 0); const { tabletDown } = useBreakpoints(); @@ -96,7 +98,7 @@ const Tabs = ({ }, }, }} - sx={tabsSharedStyles('plain')} + sx={tabsSharedStyles(appearance)} > {tabs.map( ({ label, badge, className }, index): ReactNode => ( From 940326fc2cd1002d5c146b27b359866a8e379c91 Mon Sep 17 00:00:00 2001 From: ux-git Date: Fri, 4 Sep 2026 03:58:00 +0200 Subject: [PATCH 6/7] refactor(components): let each tabs component state its own appearance The shared styles carried a default of their own, so a component that forgot to pass the appearance still got one, which is how the plain tabs came to ignore the prop in the first place. The helper asks for it now. Both components keep the appearance they render today: plain for the tabs, chip for the scrollable ones, which is what most of their callers rely on. --- src/components/tabs/index.styles.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/index.styles.ts b/src/components/tabs/index.styles.ts index 9efb962b99c..4a9ebed6d0d 100644 --- a/src/components/tabs/index.styles.ts +++ b/src/components/tabs/index.styles.ts @@ -1,8 +1,10 @@ import { SxProps, Theme } from '@mui/material'; import { TabsAppearance } from './index.types'; +// no default here on purpose: each tabs component states its own, so a missing +// prop cannot quietly pick an appearance for the caller export const tabsSharedStyles = ( - appearance: TabsAppearance = 'chip' + appearance: TabsAppearance ): SxProps => { const radius = appearance === 'chip' ? 'var(--radius-max)' : 'var(--radius-s)'; From 7876bfc9e3da2cafe534a5ef24eadd60c38361f0 Mon Sep 17 00:00:00 2001 From: ux-git Date: Fri, 4 Sep 2026 23:46:37 +0200 Subject: [PATCH 7/7] fix(ui): render component tab labels once Only text labels are duplicated to reserve their bold width; a component label would run its effects twice and repeat its ids. --- src/components/tabs/tab_label/index.tsx | 46 +++++++++++++++---------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/tabs/tab_label/index.tsx b/src/components/tabs/tab_label/index.tsx index 863c540efd4..fb60e37cb09 100644 --- a/src/components/tabs/tab_label/index.tsx +++ b/src/components/tabs/tab_label/index.tsx @@ -8,31 +8,39 @@ import Typography from '@components/typography'; * @param props The props for the TabLabel component. */ const TabLabel = ({ label, badge, selected }: TabLabelProps) => { + // only plain text is rendered twice to reserve the width of its bold + // variant: a component label would run its effects and repeat its ids + const isText = typeof label === 'string' || typeof label === 'number'; + return ( - *': { gridArea: '1 / 1' }, - }} - > + {isText ? ( *': { gridArea: '1 / 1' }, + }} > - {label} - + + {label} + - - {label} + + {label} + - + ) : ( + {label} + )} {typeof badge === 'number' && (