diff --git a/src/components/scrollable_tabs/index.tsx b/src/components/scrollable_tabs/index.tsx
index 881f6486a3a..512791a41af 100644
--- a/src/components/scrollable_tabs/index.tsx
+++ b/src/components/scrollable_tabs/index.tsx
@@ -9,10 +9,14 @@ 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';
+const TAB_HEIGHT = '48px';
+
/**
* Component that renders scrollable tabs.
*
@@ -20,15 +24,17 @@ import Typography from '@components/typography';
function ScrollableTabs({
tabs,
value,
- 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 hasIndicator = appearance === 'plain';
const [valueOfActivePanel, setValueOfActivePanel] = useState(value ?? false);
@@ -39,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();
}
@@ -69,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',
},
},
@@ -89,25 +95,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: 'var(--accent-100)',
- 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)',
},
@@ -117,17 +105,9 @@ function ScrollableTabs({
'& .MuiTabScrollButton-root': {
width: 'auto !important',
height: '36px',
- borderRadius: 'var(--radius-max)',
- transition: 'background-color 0.15s ease-in-out',
- '&:hover': {
- backgroundColor: 'var(--accent-200)',
- },
- '&:active': {
- backgroundColor: 'var(--accent-150)',
- },
},
alignItems: 'center',
- minHeight,
+ minHeight: TAB_HEIGHT,
[`& .${tabsClasses.flexContainer}`]: {
gap: '4px',
},
@@ -135,28 +115,23 @@ function ScrollableTabs({
}}
>
{tabs.map(
- ({ label, icon, className }, index): ReactNode => (
+ ({ label, badge, icon, className }, index): ReactNode => (
+ }
key={index}
className={className}
icon={icon}
iconPosition="end"
sx={{
- minHeight,
- height: minHeight,
- width:
- tabsCountOnScreen !== 0
- ? `calc(100% / ${tabsCountOnScreen})`
- : 'auto',
+ minHeight: TAB_HEIGHT,
+ height: TAB_HEIGHT,
minWidth: '20px',
- fontSize: 16,
- textTransform: 'none',
- ':not(&.Mui-selected)': { fontWeight: 400 },
- '&.Mui-Selected': {
- fontWeight: 600,
- fontSize: 18,
- },
}}
/>
)
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/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.styles.ts b/src/components/tabs/index.styles.ts
new file mode 100644
index 00000000000..4a9ebed6d0d
--- /dev/null
+++ b/src/components/tabs/index.styles.ts
@@ -0,0 +1,35 @@
+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
+): 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..0658f63918c 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';
/**
@@ -49,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();
@@ -94,21 +98,20 @@ 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(appearance)}
>
{tabs.map(
- ({ label, className }, index): ReactNode => (
+ ({ label, badge, 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..12f01709922 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.
@@ -21,10 +21,30 @@ 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';
+
+/**
+ * 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.
*/
export interface CustomTabProps extends TabOwnProps {
+ /**
+ * The look of the tabs. (Default: chip)
+ */
+ appearance?: TabsAppearance;
+
/**
* An array of tabs containing label and corresponding component.
*/
@@ -34,6 +54,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.
*/
@@ -52,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.
@@ -71,22 +91,9 @@ export interface CustomTabProps extends TabOwnProps {
className?: string;
/**
- * The variant of the tabs, aligning with the MUI `TabsOwnProps` variant.
- * Examples include `"scrollable"` or `"standard"`.
- */
- variant?: TabsOwnProps['variant'];
-
- /**
- * Minimum height for the tab component.
- * Useful for ensuring consistent tab sizes.
- */
- minHeight?: string;
-
- /**
- * The number of tabs that should be displayed on the screen at once.
- * Useful for responsive layouts or custom tab implementations.
+ * The width of the tabs. (Default: auto)
*/
- tabsCountOnScreen?: number;
+ layout?: TabsLayout;
/**
* 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
new file mode 100644
index 00000000000..fb60e37cb09
--- /dev/null
+++ b/src/components/tabs/tab_label/index.tsx
@@ -0,0 +1,66 @@
+import { Box } from '@mui/material';
+import { TabLabelProps } from './index.types';
+import Typography from '@components/typography';
+
+/**
+ * A custom tab label component.
+ *
+ * @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 (
+
+ {isText ? (
+ *': { gridArea: '1 / 1' },
+ }}
+ >
+
+ {label}
+
+
+
+ {label}
+
+
+ ) : (
+ {label}
+ )}
+
+ {typeof badge === 'number' && (
+
+
+ {badge}
+
+
+ )}
+
+ );
+};
+
+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..b4da56a4b73
--- /dev/null
+++ b/src/components/tabs/tab_label/index.types.ts
@@ -0,0 +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/applications/index.tsx b/src/features/persons/applications/index.tsx
index 6f4002b3792..b31ece88665 100644
--- a/src/features/persons/applications/index.tsx
+++ b/src/features/persons/applications/index.tsx
@@ -32,18 +32,10 @@ const PersonApplications = () => {
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: (
{
>
-
+
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 (
{
-
+
>
);
};
diff --git a/src/features/reports/publisher_records/publisher_tabs/index.tsx b/src/features/reports/publisher_records/publisher_tabs/index.tsx
index 04dedccb4a9..9b5924e4ffb 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/features/reports/publisher_records/publisher_tabs/usePublisherTabs.tsx b/src/features/reports/publisher_records/publisher_tabs/usePublisherTabs.tsx
index 1af1e260d17..4e594d51d00 100644
--- a/src/features/reports/publisher_records/publisher_tabs/usePublisherTabs.tsx
+++ b/src/features/reports/publisher_records/publisher_tabs/usePublisherTabs.tsx
@@ -5,7 +5,6 @@ import { personsActiveState } from '@states/persons';
import { PersonType } from '@definition/person';
import usePerson from '@features/persons/hooks/usePerson';
import ListByGroups from './list_by_groups';
-import TabLabelWithBadge from '@components/tab_label_with_badge';
const usePublisherTabs = () => {
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: ,
},
];
diff --git a/src/features/reports/publisher_records/years_stats/index.tsx b/src/features/reports/publisher_records/years_stats/index.tsx
index d8af5dd0117..4732f983a06 100644
--- a/src/features/reports/publisher_records/years_stats/index.tsx
+++ b/src/features/reports/publisher_records/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_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')}
-
+
);
};
diff --git a/src/pages/meetings/schedules/index.tsx b/src/pages/meetings/schedules/index.tsx
index e5e07137b76..8832002b020 100644
--- a/src/pages/meetings/schedules/index.tsx
+++ b/src/pages/meetings/schedules/index.tsx
@@ -30,26 +30,10 @@ const WeeklySchedules = () => {
}}
>