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 = () => { }} >