diff --git a/components/AboutPagesCard/AboutPagesCard.tsx b/components/AboutPagesCard/AboutPagesCard.tsx index 6fbf18b41..ef6c74903 100644 --- a/components/AboutPagesCard/AboutPagesCard.tsx +++ b/components/AboutPagesCard/AboutPagesCard.tsx @@ -4,6 +4,9 @@ import { FC, PropsWithChildren } from "react" const StyledHeader = styled(Card.Header)` transform: translate(0); + font-family: "Nunito", system-ui, -apple-system, "Segoe UI", sans-serif; + font-weight: 700; + font-size: 1.25rem; &:first-child { border-radius: 10px 10px 0 0; @@ -20,19 +23,25 @@ const StyledHeader = styled(Card.Header)` } ` +const Body = styled(Card.Body)` + font-size: 1rem; + line-height: 1.6; + color: var(--maple-text-body); +` + const AboutPagesCard: FC> = ({ title, children }) => { return ( - + {title} - {children} + {children} ) } diff --git a/components/AdditionalResources.tsx b/components/AdditionalResources.tsx index 1e6170ad8..144f99051 100644 --- a/components/AdditionalResources.tsx +++ b/components/AdditionalResources.tsx @@ -1,48 +1,112 @@ -import { Container } from "react-bootstrap" import { useTranslation, Trans } from "next-i18next" -import * as links from "components/links" +import styled from "styled-components" -const AdditionalResources = () => { - const content = [ - { - i18nKey: "find_legislator", - href: "https://malegislature.gov/Search/FindMyLegislator" - }, - { - i18nKey: "legislative_doc", - href: "https://www.mass.gov/doc/the-legislative-process-0/download" - }, - { - i18nKey: "legal_services", - href: "https://www.masslegalservices.org/content/legislative-process-massachusetts-0" +const Section = styled.section` + margin-top: 3rem; + + h2 { + font-family: var(--maple-font-heading); + font-weight: 700; + color: var(--bs-blue); + font-size: clamp(1.5rem, 4vw, 1.875rem); + margin-bottom: 0.75rem; + } + + .intro { + color: var(--maple-text-body); + line-height: 1.7; + margin-bottom: 1.5rem; + } + + ul { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin: 0; + padding: 0; + list-style: none; + } + + li { + background: var(--maple-surface-base); + border-radius: var(--maple-radius-xl); + box-shadow: var(--maple-shadow-sm); + padding: 1.25rem 1.5rem; + transition: box-shadow 0.2s ease; + + &:hover { + box-shadow: var(--maple-shadow-hover); + } + } + + p { + color: var(--maple-text-body); + line-height: 1.7; + margin: 0; + } + + a { + color: var(--bs-blue); + text-decoration: underline; + } + + @media (prefers-reduced-motion: reduce) { + li { + transition: none; } - ] + } +` + +const content = [ + { + i18nKey: "find_legislator", + href: "https://malegislature.gov/Search/FindMyLegislator" + }, + { + i18nKey: "legislative_doc", + href: "https://www.mass.gov/doc/the-legislative-process" + }, + { + i18nKey: "legal_services", + href: "https://www.masslegalservices.org/content/legislative-process-massachusetts-0" + } +] + +const AdditionalResources = () => { const { t } = useTranslation("learnComponents") return ( - -

+
+ {/* h2, not h1: the page already has one, and this is a subsection of it. */} +

{t("legislative.additional_resources")} -

-

{t("legislative.resources_intro")}

- - {content.map(({ i18nKey, href }) => ( -
-
-

+

+

{t("legislative.resources_intro")}

+ +
    + {content.map(({ i18nKey, href }) => ( +
  • +

    + {/* A bare anchor, not links.External: that helper appends a + trailing space, which shows up as "the process ." here. */} ]} + components={[ + // eslint-disable-next-line jsx-a11y/anchor-has-content + + ]} />

    - - - ))} - +
  • + ))} +
+ ) } diff --git a/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx b/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx deleted file mode 100644 index cc667c3d4..000000000 --- a/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { Row, Col } from "../bootstrap" -import Image from "react-bootstrap/Image" -import styled from "styled-components" -import { useTranslation } from "next-i18next" - -const StyledImage = styled(Image)` - width: 12.5rem; - /* bootstrap: medium > 768px */ - @media (max-width: 48em) { - display: flex; - margin-left: auto; - margin-right: auto; - } - - /* bootstrap: small > 576px */ - @media (max-width: 36em) { - width: 10rem; - } - - /* bootstrap: xs < 576px - this break at 464px */ - @media (max-width: 26em) { - width: 8rem; - } -` - -const WritingContent = () => ( - - -

- {useTranslation("learnComponents").t( - "communicating.testifyInWriting.content" - )} -

- - - - -
-) - -const OralContent = () => ( - - - - - -

- {useTranslation("learnComponents").t( - "communicating.testifyOrally.content" - )} -

- -
-) - -const WriteOrCallContent = () => ( - - -

- {useTranslation("learnComponents").t( - "communicating.writeOrCall.content" - )} -

- - - - -
-) - -export { WritingContent, OralContent, WriteOrCallContent } diff --git a/components/Faq/FaqCard.tsx b/components/Faq/FaqCard.tsx index 9c9331099..4487b57e7 100755 --- a/components/Faq/FaqCard.tsx +++ b/components/Faq/FaqCard.tsx @@ -1,4 +1,6 @@ +import { useId, useState } from "react" import styled from "styled-components" +import { ChevronDownIcon } from "../learn/icons" import { FaqQandAButton } from "./FaqQandAButton" type faqCardProps = { @@ -6,33 +8,151 @@ type faqCardProps = { qAndAs: { disabled?: boolean; question: string; answer: string }[] } -export const FaqCard = ({ heading, qAndAs }: faqCardProps) => { - const FaqCardContainer = styled.div` - margin: 0 2rem; - padding: 2rem 2.5rem; +const Card = styled.section` + background: var(--maple-surface-base); + border-radius: var(--maple-radius-xl); + box-shadow: var(--maple-shadow-sm); + overflow: hidden; + + > h2 { + font-family: var(--maple-font-heading); + font-weight: 700; + color: var(--bs-blue); + font-size: 1.125rem; + margin: 0; + } + + > h2 > button { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 1.5rem 2rem; + background: none; + border: 0; + cursor: pointer; + font: inherit; + color: inherit; + text-align: left; + + &:focus-visible { + outline: 2px solid var(--bs-blue); + outline-offset: -2px; + } + } - @media only screen and (max-width: 600px) { - margin: 0rem; - padding: 2rem 1.5rem; + /* Chevron size and animation copied from the Why Use MAPLE toggles: a single + chevron that rotates rather than swapping icons. No hover circle here -- + the cursor alone is enough to signal the whole header is clickable. */ + .chevron { + flex-shrink: 0; + color: var(--maple-text-muted); + transition: transform 0.25s ease; + } + + > button[aria-expanded="true"] .chevron { + transform: rotate(180deg); + } + + @media (prefers-reduced-motion: reduce) { + .chevron { + transition: none; } - ` + } + + /* Expand/collapse animation copied from the Why Use MAPLE toggles: the row + animates between 0fr and 1fr rather than mounting instantly. The inner + element clips so the padding collapses with it. */ + .items { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + visibility: hidden; + transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.2s ease, visibility 0s linear 0.35s; + } + + .items[data-open="true"] { + grid-template-rows: 1fr; + opacity: 1; + visibility: visible; + transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.25s ease 0.08s, visibility 0s; + } + + /* No padding here: overflow: hidden only clips overflowing content, not this + element's own padding, so any padding placed directly on it would still + render at full size even while collapsed. Padding lives on .items-content + instead, which is what actually gets clipped away. */ + .items-inner { + min-height: 0; + overflow: hidden; + } + + /* Horizontal inset lives on each question row instead (see FaqQandAButton), + so the divider and the hover background share the same bounds as the row's + own padding rather than the container's. */ + .items-content { + padding: 0 0 1rem; + } + + @media (prefers-reduced-motion: reduce) { + .items { + transition: none; + } + } + + @media (max-width: 36rem) { + > button { + padding: 1.25rem 1.25rem; + } + + .items-content { + padding: 0 0 0.75rem; + } + } +` + +/** A collapsible category of questions. Categories start expanded. */ +export const FaqCard = ({ heading, qAndAs }: faqCardProps) => { + const [open, setOpen] = useState(true) + const panelId = useId() + const questions = qAndAs.filter(q => !q.disabled) return ( - -
{heading ?? ""}
-
- {qAndAs.map( - (key, index) => - typeof key.disabled === "undefined" && ( -
+ + {/* The heading wraps the button (WAI-ARIA accordion pattern) so the + category is exposed as a real h2 -- a heading nested inside the button + would be flattened into the button's label and lost. */} +

+ +

+
+
+
+ {questions.map(q => ( -
- ) - )} - + key={q.question} + question={q.question} + answer={q.answer} + /> + ))} +
+
+
+
) } diff --git a/components/Faq/FaqPage.tsx b/components/Faq/FaqPage.tsx index fc0a6e64b..42a2d01c8 100644 --- a/components/Faq/FaqPage.tsx +++ b/components/Faq/FaqPage.tsx @@ -1,43 +1,93 @@ +import { useTranslation } from "next-i18next" import styled from "styled-components" -import { Container, Stack } from "../bootstrap" +import LearnBreadcrumb from "../learn/LearnBreadcrumb" +import LearnHeader from "../learn/LearnHeader" +import LearnLayout from "../learn/LearnLayout" import { FaqCard } from "./FaqCard" import content from "./faqContent.json" -export const FaqPage = () => { - const faqData: FaqData = content - const faqKeys: string[] = Object.keys(faqData) +type Category = { + heading: string + qAndA: { disabled?: boolean; question: string; answer: string }[] +} + +const isCategory = (v: unknown): v is Category => + typeof v === "object" && v !== null && "qAndA" in v + +const Cards = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +` + +// Closing "still have questions?" call-to-action, on the tinted page background: +// a centered card with a mailto pill button. +const Cta = styled.div` + background: var(--maple-surface-base); + border-radius: var(--maple-radius-xl); + box-shadow: var(--maple-shadow-sm); + padding: 2.5rem 2rem; + margin-top: 1.5rem; + text-align: center; - interface FaqData { - [key: string]: string | FaqDataCard | any + h2 { + font-family: var(--maple-font-heading); + font-weight: 900; + color: var(--bs-blue); + font-size: 1.5rem; + margin-bottom: 0.5rem; } - interface FaqDataCard { - heading: string - qAndA: { question: string; answer: string }[] + p { + color: var(--maple-text-body); + line-height: 1.65; + max-width: 34rem; + margin: 0 auto 1.5rem; } - const FaqH1 = styled.h1` - margin: 1.5rem 0rem 1.5rem 2rem; + a { + display: inline-block; + padding: var(--maple-space-sm) var(--maple-space-xl); + border-radius: var(--maple-radius-pill); + background: var(--maple-brand-primary); + color: var(--maple-text-inverse); + font-weight: 700; + text-decoration: none; + } + + a:hover { + background: var(--maple-brand-primary-strong); + color: var(--maple-text-inverse); + } +` - @media only screen and (max-width: 600px) { - margin: 1rem 0rem 1rem 0rem; - } - ` +export const FaqPage = () => { + const { t } = useTranslation("common") + const categories = Object.values(content).filter(isCategory) return ( - <> - - FAQ - - {faqKeys.map((key, index) => ( - - ))} - - - + + + + + {categories.map(category => ( + + ))} + + +

{content.ctaHeadline}

+

{content.ctaBody}

+
{content.ctaButton} + + ) } diff --git a/components/Faq/FaqQandAButton.tsx b/components/Faq/FaqQandAButton.tsx index fa093770b..2bd065f75 100644 --- a/components/Faq/FaqQandAButton.tsx +++ b/components/Faq/FaqQandAButton.tsx @@ -1,52 +1,156 @@ -import { useEffect, useState } from "react" -import { Image, Collapse } from "../bootstrap" -import { useTranslation } from "next-i18next" +import { useId, useState } from "react" +import AddIcon from "@mui/icons-material/Add" +import RemoveIcon from "@mui/icons-material/Remove" +import styled from "styled-components" +import { Internal } from "../links" type faqQandAProps = { question: string answer: string } -export const FaqQandAButton = ({ question, answer }: faqQandAProps) => { - const { t } = useTranslation("common") - const [open, setOpen] = useState(false) +/* Full width: the divider spans the whole card, and the hover background + (painted on the button) reaches the same edges. The inset from the card's + edges is the button's own horizontal padding, not a margin on this row. + + An inset-divider alternative was tried and set aside (kept below, commented + out, in case it's wanted again): a wrapped line that fills only its parent's + content box, so padding on the wrapper shortens the visible line without a + margin -- border-top can't be inset this way, since a border draws at the + outer edge of the padding box regardless of that box's own padding. */ +const Item = styled.div` + border-top: 1px solid var(--maple-surface-border); + + /* .divider-wrap { + padding: 0 2rem; + } + + .divider-line { + height: 1px; + background-color: var(--maple-surface-border); + } + + @media (max-width: 36rem) { + .divider-wrap { + padding: 0 1.25rem; + } + } */ + + button { + width: 100%; + display: flex; + align-items: center; + gap: 0.875rem; + padding: 1.125rem 2rem; + background: none; + border: 0; + text-align: left; + cursor: pointer; + transition: background-color 0.15s ease; + + &:hover { + background-color: var(--maple-surface-muted); + } + + &:focus-visible { + outline: 2px solid var(--bs-blue); + outline-offset: 2px; + } + } - useEffect(() => {}, [open]) + @media (max-width: 36rem) { + button { + padding: 1.125rem 1.25rem; + } + } + + .badge { + flex-shrink: 0; + width: 1.5rem; + height: 1.5rem; + border-radius: var(--maple-radius-pill); + background: var(--maple-surface-learn); + color: var(--maple-text-muted); + display: flex; + align-items: center; + justify-content: center; + } + + .question { + flex: 1; + min-width: 0; + font-weight: 600; + color: var(--maple-text-strong); + line-height: 1.45; + } + + .answer { + color: var(--maple-text-muted); + line-height: 1.6; + /* The answer sits outside the button, so its indent has to repeat the + button's own left padding (2rem) plus badge width (1.5rem) and gap + (0.875rem) to land under the question text; the right margin mirrors the + button's right padding. */ + margin: 0 2rem 1.125rem 4.375rem; + } - let supportLink = null + .answer a { + color: var(--bs-blue); + text-decoration: underline; + } - { - question == "How can I support MAPLE?" - ? (supportLink = ( - - {" this page"} - - )) - : null + @media (max-width: 36rem) { + .answer { + margin: 0 1.25rem 1.125rem 3.625rem; + } } +` + +/** + * A single question that expands to reveal its answer. The one about supporting + * MAPLE links to the support page, so that answer is composed rather than plain + * text. + */ +export const FaqQandAButton = ({ question, answer }: faqQandAProps) => { + const [open, setOpen] = useState(false) + const panelId = useId() + + const isSupport = question === "How can I support MAPLE?" return ( - <> - setOpen(!open)} - aria-controls="example-collapse-text" + + {/* Inset-divider alternative -- pairs with the commented CSS above. */} + {/*
+
+
*/} +
+ {/* Kept mounted (hidden when collapsed) so the button's aria-controls + always references an element that exists in the DOM. */} + + ) } diff --git a/components/Faq/faqContent.json b/components/Faq/faqContent.json index 1010e8ca3..6b1bff45c 100755 --- a/components/Faq/faqContent.json +++ b/components/Faq/faqContent.json @@ -1,4 +1,11 @@ { + "title": "Frequently Asked Questions", + "breadcrumb": "FAQ", + "subhead": "Common questions about MAPLE, testimony, accounts, and privacy.", + "ctaHeadline": "Still have a question?", + "ctaBody": "If you don't see what you are looking for please reach out with any questions.", + "ctaEmail": "admin@mapletestimony.org", + "ctaButton": "Email Us", "card1": { "heading": "General", "qAndA": [ diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx index ce5063afa..98969fb18 100644 --- a/components/Footer/Footer.tsx +++ b/components/Footer/Footer.tsx @@ -189,7 +189,7 @@ const LearnLinks = () => { const { t } = useTranslation(["footer", "common"]) return ( <> - + {t("links.learnWriting")} @@ -198,6 +198,9 @@ const LearnLinks = () => { {t("links.learnWhy")} + + {t("links.learnAi")} + ) } diff --git a/components/GoalsAndMission/GoalsAndMission.js b/components/GoalsAndMission/GoalsAndMission.js index dea7ca66a..3a397b31b 100644 --- a/components/GoalsAndMission/GoalsAndMission.js +++ b/components/GoalsAndMission/GoalsAndMission.js @@ -1,31 +1,26 @@ -import { Container, Row, Col } from "../bootstrap" import AboutPagesCard from "../AboutPagesCard/AboutPagesCard" import { OurGoalsCardContent, OurMissionCardContent } from "../GoalsAndMissionCardContent/GoalsAndMissionCardContent" import { useTranslation } from "next-i18next" +import LearnBreadcrumb from "../learn/LearnBreadcrumb" +import LearnHeader from "../learn/LearnHeader" +import LearnLayout from "../learn/LearnLayout" const GoalsAndMission = () => { - const { t } = useTranslation("goalsandmission") + const { t } = useTranslation(["goalsandmission", "common"]) return ( - - - -

- {t("header")} -

- - - - - - - -
-
+ + + + + + + + + + ) } diff --git a/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx b/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx index 70162e874..da93fd4db 100644 --- a/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx +++ b/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx @@ -2,7 +2,7 @@ import { Row, Col } from "../bootstrap" import Image from "react-bootstrap/Image" import { SignInWithButton } from "../auth" import * as links from "../links" -import { Trans, useTranslation } from "next-i18next" +import { useTranslation } from "next-i18next" import { useAuth } from "components/auth" import styled from "styled-components" @@ -20,20 +20,96 @@ const StepsImage = styled(Image)` } ` +// Goal captions with tighter line spacing than the body default. (Utility +// classes on the element still handle alignment/size/weight/spacing.) +const GoalCaption = styled.figcaption` + line-height: 1.2; +` + +// Closing call-to-action at the bottom of the mission card: a centered headline, +// lede, a primary sign-in pill and a secondary outlined link. +const Cta = styled.div` + margin-top: 2.5rem; + padding-bottom: 1.5rem; + text-align: center; + + h3 { + font-family: var(--maple-font-heading); + font-weight: 900; + color: var(--bs-blue); + font-size: 1.5rem; + margin-bottom: 0.5rem; + } + + .lede { + color: var(--maple-text-body); + line-height: 1.65; + max-width: 34rem; + margin: 0 auto 1.5rem; + } + + .cta-action, + a.cta-action, + button.cta-action { + display: inline-block; + width: auto !important; + padding: var(--maple-space-sm) var(--maple-space-xl); + border-radius: var(--maple-radius-pill); + background: var(--maple-brand-primary) !important; + border: 2px solid var(--maple-brand-primary) !important; + color: var(--maple-text-inverse) !important; + font-weight: 700; + text-decoration: none; + cursor: pointer; + } + + .cta-action:hover, + a.cta-action:hover, + button.cta-action:hover { + background: var(--maple-brand-primary-strong) !important; + border-color: var(--maple-brand-primary-strong) !important; + color: var(--maple-text-inverse) !important; + } + + .cta-action-wrap { + display: inline-block; + } + + .cta-buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; + } + + /* Secondary action: the same pill, outlined rather than filled. */ + .cta-action-secondary, + a.cta-action-secondary { + background: transparent !important; + color: var(--maple-brand-primary) !important; + } + + .cta-action-secondary:hover, + a.cta-action-secondary:hover { + background: var(--maple-brand-primary) !important; + color: var(--maple-text-inverse) !important; + } +` + const OurGoalsCardContent = () => { const { t } = useTranslation("goalsandmission") return ( <> -
+

{t("goals.overview")} -

+
-
+ {t("goals.increase")} -
+
@@ -44,9 +120,9 @@ const OurGoalsCardContent = () => { src="/doc-with-arrows-from-people.svg" alt="" /> -
+ {t("goals.engage")} -
+
@@ -55,18 +131,18 @@ const OurGoalsCardContent = () => {
-
+ {t("goals.strengthen")} -
+
-
+ {t("goals.encourage")} -
+
@@ -80,12 +156,15 @@ const OurMissionCardContent = () => { return ( <> -
+

{t("mission.overview")} -

- + + -

+

{t("mission.connect")}

@@ -112,7 +191,8 @@ const OurMissionCardContent = () => { sm={{ span: 12, order: 2 }} md={{ span: 6, order: 1 }} lg={8} - className={`fs-6 fs-sm-5 tracking-tight text-start lh-sm pt-4 pb-3 pb-sm-4 p-md-0`} + className={`fs-6 text-start lh-sm p-3 p-md-4`} + style={{ letterSpacing: "0.005em", fontSize: "0.875rem" }} >

{t("mission.disclosure")} @@ -125,42 +205,39 @@ const OurMissionCardContent = () => {

- - - -

- {t("mission.publish1")} -

-

- {t("mission.publish2")} -

+

{t("mission.publish1")}

+

{t("mission.publish2")}

+
{!authenticated && ( - <> - - -

- {t("mission.submit_now")} -

- -
- - - - - - - + +

{t("mission.ctaHeadline")}

+

{t("mission.ctaBody")}

+
+ + + {t("mission.ctaSecondary")} + +
+
)} ) diff --git a/components/InTheNews/InTheNews.tsx b/components/InTheNews/InTheNews.tsx index 6fd4c04fe..495c24a39 100644 --- a/components/InTheNews/InTheNews.tsx +++ b/components/InTheNews/InTheNews.tsx @@ -1,5 +1,5 @@ import { useState, Fragment } from "react" -import { Col, Row, Container, Badge, Spinner } from "../bootstrap" +import { Col, Row, Badge, Spinner } from "../bootstrap" import Tab from "react-bootstrap/Tab" import Nav from "react-bootstrap/Nav" import Dropdown from "react-bootstrap/Dropdown" @@ -7,6 +7,9 @@ import { useMediaQuery } from "usehooks-ts" import { useTranslation } from "next-i18next" import { NewsCard } from "./NewsCard" import { NewsType, NewsItem, useNews } from "components/db/news" +import LearnBreadcrumb from "../learn/LearnBreadcrumb" +import LearnHeader from "../learn/LearnHeader" +import LearnLayout from "../learn/LearnLayout" type NewsFeedProps = { type: NewsType | null @@ -22,7 +25,10 @@ type TabCounts = { const NewsFeed = ({ type, newsItems }: NewsFeedProps) => { return ( -
+
{newsItems .filter(item => item.type === type || type === null) .map((item, index) => ( @@ -33,7 +39,7 @@ const NewsFeed = ({ type, newsItems }: NewsFeedProps) => { } export const InTheNews = () => { - const { t } = useTranslation("inTheNews") + const { t } = useTranslation(["inTheNews", "common"]) const isMobile = useMediaQuery("(max-width: 768px)") const { result: newsItems, loading } = useNews() @@ -46,11 +52,15 @@ export const InTheNews = () => { : null return ( - -

- {t("title")} -

-
+ + + +
{loading ? (
@@ -72,42 +82,30 @@ export const InTheNews = () => { -
- -
+
-
- -
+
-
- -
+
) : ( - - -
- -
- -
+ )}
- + ) } const TabGroup = ({ counts }: { counts: TabCounts }) => { const { t } = useTranslation("inTheNews") return ( - + {newsTypePlurals .filter(val => counts[val]) .map(val => ( @@ -115,13 +113,13 @@ const TabGroup = ({ counts }: { counts: TabCounts }) => {