From bbe7720e5e4863aed72def78e69f1d04582b90b2 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 10 Jun 2026 16:44:17 +0100 Subject: [PATCH 01/25] feat(ipa): Add review checklist component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders a guideline's manual evaluation steps as an interactive review checklist inside a collapsible accordion: per-step checkboxes, a live "N of M verified" progress chip in the header, and a reset control. Progress is page-local only. - / compound component built on ui/Accordion - "Reference only — agent skips" marker when the parent guideline is lintable (steps document what Spectral checks) - Dev-console warning when an unlintable, non-informational lacks a (cross-field check; build-time enforcement lands with the validation plugin) - Fixtures section + tests CLOUDP-399874 --- ipa/dev/component-fixtures.mdx | 74 +++++- src/components/ipa/Guideline/index.tsx | 23 +- .../ipa/Workflow/Workflow.module.css | 113 +++++++++ src/components/ipa/Workflow/Workflow.test.tsx | 217 ++++++++++++++++++ src/components/ipa/Workflow/index.tsx | 129 +++++++++++ src/components/ipa/index.ts | 1 + src/hooks/useGuideline.ts | 3 + 7 files changed, 557 insertions(+), 3 deletions(-) create mode 100644 src/components/ipa/Workflow/Workflow.module.css create mode 100644 src/components/ipa/Workflow/Workflow.test.tsx create mode 100644 src/components/ipa/Workflow/index.tsx diff --git a/ipa/dev/component-fixtures.mdx b/ipa/dev/component-fixtures.mdx index d95b74b..437dfe5 100644 --- a/ipa/dev/component-fixtures.mdx +++ b/ipa/dev/component-fixtures.mdx @@ -9,7 +9,12 @@ description: production builds." --- -import { Guidelines, Guideline, Example } from "@site/src/components/ipa"; +import { + Guidelines, + Guideline, + Example, + Workflow, +} from "@site/src/components/ipa"; # Component fixtures @@ -113,3 +118,70 @@ name: list-resources ``` + +## `` + +`` renders the manual evaluation steps of a guideline as an +interactive review checklist inside a collapsible accordion. Checking steps +updates the progress chip in the header; progress is page-local only. + +### Inside an unlintable guideline + +The primary use: unlintable, non-informational guidelines must document their +review steps (a dev-console warning fires when they don't). + + + API producers must use American English for all user-facing text in the API + specification. + + + + Scan every user-facing string in the specification (descriptions, + summaries, enum values, field names). + + + For each string, check for common British English spellings (e.g., + "cancelled", "colour", "authorise", "behaviour", "organisation"). + + + Consult Merriam-Webster's dictionary for the preferred American English + form. + + + Flag any non-American spelling as a violation. + + + + +### Inside a lintable guideline (reference only) + +When the parent guideline is lintable, the workflow renders a "Reference only — +agent skips" marker: the steps document what Spectral checks. + + + Lintable guidelines may still include a workflow for human readers. + + + Verify the operation defines a summary. + Verify the summary uses Title Case. + + + +### Standalone with a custom title + + + Add the field to the OpenAPI schema. + Regenerate the SDK clients. + Update the changelog entry. + diff --git a/src/components/ipa/Guideline/index.tsx b/src/components/ipa/Guideline/index.tsx index c2fd958..23e6943 100644 --- a/src/components/ipa/Guideline/index.tsx +++ b/src/components/ipa/Guideline/index.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, type ReactElement } from "react"; +import { type ReactNode, type ReactElement, useEffect, useRef } from "react"; import clsx from "clsx"; import type { Guideline } from "../../../types/guideline"; import { GuidelineContext } from "../../../hooks/useGuideline"; @@ -17,9 +17,28 @@ export function Guideline({ }: GuidelineProps): ReactElement { const isInsideGuidelines = useIsInsideGuidelines(); const Root = isInsideGuidelines ? "li" : "div"; + const hasWorkflow = useRef(false); + + useEffect(() => { + if (process.env.NODE_ENV === "production") return; + if (guideline.lintable || guideline.informational) return; + if (!hasWorkflow.current) { + console.warn( + `Guideline ${guideline.id} is unlintable and non-informational but ` + + "has no documenting its manual review steps.", + ); + } + }, [guideline.id, guideline.lintable, guideline.informational]); return ( - + { + hasWorkflow.current = true; + }, + }} + > ({ + useDoc: () => ({ frontMatter: { id: 1, state: "adopt" } }), +})); + +const lintableGuideline = { + id: "IPA-0001-must-test-a", + lintable: true, + informational: false, + implementation: false, + effort: "check", + given: "spec", +} satisfies GuidelineData; + +const unlintableGuideline = { + ...lintableGuideline, + lintable: false, +} satisfies GuidelineData; + +describe("", () => { + it("renders a collapsed accordion titled 'Evaluation workflow'", () => { + render( + + Scan every user-facing string. + , + ); + const toggle = screen.getByRole("button", { + name: /evaluation workflow/i, + }); + + expect(toggle).toHaveAttribute("aria-expanded", "false"); + }); + + it("renders each step as a list item of an ordered list", () => { + render( + + first step + second step + , + ); + const list = screen.getByTestId("workflow-steps"); + + expect(list.tagName).toBe("OL"); + expect(list.children).toHaveLength(2); + expect(list.children[0].tagName).toBe("LI"); + expect(screen.getByText("first step").closest("li")).toBeInTheDocument(); + }); + + it("renders a custom title instead of the default", () => { + render( + + only step + , + ); + + expect( + screen.getByRole("button", { name: /implementation steps/i }), + ).toBeInTheDocument(); + expect(screen.queryByText(/evaluation workflow/i)).toBeNull(); + }); +}); + +describe(" review checklist", () => { + it("renders an unchecked checkbox for each step", () => { + render( + + first step + second step + , + ); + const checkboxes = screen.getAllByRole("checkbox"); + + expect(checkboxes).toHaveLength(2); + checkboxes.forEach((box) => expect(box).not.toBeChecked()); + }); + + it("updates the progress chip when a step is verified", () => { + render( + + first step + second step + , + ); + + expect(screen.getByText("0 of 2 verified")).toBeInTheDocument(); + + fireEvent.click(screen.getAllByRole("checkbox")[0]); + + expect(screen.getByText("1 of 2 verified")).toBeInTheDocument(); + }); + + it("turns the progress chip green only when all steps are verified", () => { + render( + + first step + second step + , + ); + const chip = () => screen.getByTestId("workflow-progress"); + + expect(chip().querySelector("[data-color='muted']")).not.toBeNull(); + + screen.getAllByRole("checkbox").forEach((box) => fireEvent.click(box)); + + expect(screen.getByText("2 of 2 verified")).toBeInTheDocument(); + expect(chip().querySelector("[data-color='green']")).not.toBeNull(); + }); + + it("resets all verified steps", () => { + render( + + first step + second step + , + ); + const reset = screen.getByRole("button", { name: /reset/i }); + + expect(reset).toBeDisabled(); + + fireEvent.click(screen.getAllByRole("checkbox")[0]); + expect(reset).toBeEnabled(); + + fireEvent.click(reset); + + expect(screen.getByText("0 of 2 verified")).toBeInTheDocument(); + screen + .getAllByRole("checkbox") + .forEach((box) => expect(box).not.toBeChecked()); + }); +}); + +describe(" inside a lintable guideline", () => { + it("shows a reference-only marker when the parent guideline is lintable", () => { + render( + + + only step + + , + ); + + expect( + screen.getByText(/reference only — agent skips/i), + ).toBeInTheDocument(); + }); + + it("shows no marker when the parent guideline is unlintable", () => { + render( + + + only step + + , + ); + + expect(screen.queryByText(/reference only/i)).toBeNull(); + }); + + it("shows no marker when rendered outside a guideline", () => { + render( + + only step + , + ); + + expect(screen.queryByText(/reference only/i)).toBeNull(); + }); +}); + +describe(" workflow cross-field check", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("warns in development when an unlintable, non-informational guideline lacks a ", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + render(body); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("IPA-0001-must-test-a"), + ); + }); + + it("does not warn when the guideline contains a ", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + render( + + + only step + + , + ); + + expect(warn).not.toHaveBeenCalled(); + }); + + it("does not warn for lintable or informational guidelines", () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + render(body); + render( + + body + , + ); + + expect(warn).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/ipa/Workflow/index.tsx b/src/components/ipa/Workflow/index.tsx new file mode 100644 index 0000000..f80b4c3 --- /dev/null +++ b/src/components/ipa/Workflow/index.tsx @@ -0,0 +1,129 @@ +import { + Children, + cloneElement, + isValidElement, + useContext, + useEffect, + useState, + type ReactNode, + type ReactElement, +} from "react"; +import clsx from "clsx"; +import { Accordion, Badge } from "../../ui"; +import { GuidelineContext } from "../../../hooks/useGuideline"; +import styles from "./Workflow.module.css"; + +interface WorkflowProps { + title?: string; + children: ReactNode; +} + +interface WorkflowStepProps { + children: ReactNode; + // Injected by — not part of the authoring API. + checked?: boolean; + onToggle?: () => void; +} + +function WorkflowStep({ + children, + checked = false, + onToggle, +}: WorkflowStepProps): ReactElement { + return ( +
  • + +
  • + ); +} +WorkflowStep.displayName = "Workflow.Step"; + +function WorkflowBase({ title, children }: WorkflowProps): ReactElement { + // Optional on purpose: also renders standalone (e.g. fixtures). + const guidelineCtx = useContext(GuidelineContext); + const referenceOnly = guidelineCtx?.guideline.lintable ?? false; + + useEffect(() => { + guidelineCtx?.reportWorkflow?.(); + }, [guidelineCtx]); + + const steps = Children.toArray(children).filter( + (child): child is ReactElement => + isValidElement(child) && child.type === WorkflowStep, + ); + const [verified, setVerified] = useState>(new Set()); + + const toggleStep = (index: number) => + setVerified((prev) => { + const next = new Set(prev); + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + return next; + }); + + const total = steps.length; + const done = verified.size; + const allDone = total > 0 && done === total; + + return ( + + {title ?? "Evaluation workflow"} + {referenceOnly && ( + + Reference only — agent skips + + )} + + + {done} of {total} verified + + + + } + titleClassName={styles.header} + contentClassName={styles.content} + > +
      + {steps.map((step, index) => + cloneElement(step, { + key: index, + checked: verified.has(index), + onToggle: () => toggleStep(index), + }), + )} +
    +
    + +
    +
    + ); +} + +export const Workflow = Object.assign(WorkflowBase, { + Step: WorkflowStep, +}); diff --git a/src/components/ipa/index.ts b/src/components/ipa/index.ts index c8bdd23..e2e30a4 100644 --- a/src/components/ipa/index.ts +++ b/src/components/ipa/index.ts @@ -2,3 +2,4 @@ export { Guidelines } from "./Guidelines"; export { Guideline } from "./Guideline"; export { PrincipleHeader } from "./PrincipleHeader"; export { Example } from "./Example"; +export { Workflow } from "./Workflow"; diff --git a/src/hooks/useGuideline.ts b/src/hooks/useGuideline.ts index 7d30939..cc36fc1 100644 --- a/src/hooks/useGuideline.ts +++ b/src/hooks/useGuideline.ts @@ -3,6 +3,9 @@ import type { Guideline } from "../types/guideline"; interface GuidelineContextValue { guideline: Guideline; + // Lets a child report its presence so can flag + // unlintable, non-informational guidelines that lack review steps. + reportWorkflow?: () => void; } export const GuidelineContext = createContext( From b09297a54f2684107f64bb9a8da577b41d1634ff Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Wed, 10 Jun 2026 16:59:37 +0100 Subject: [PATCH 02/25] feat(ipa): Restyle to the approved checklist design Port the variant-4 mockup faithfully: checkbox-in-square title icon, pill-shaped progress chip with dot, gradient progress track, page-local hint line, full-width hairline step separators, custom animated checkboxes, and step number circles echoing the numbering motif. Also fix : the header separator only renders while expanded, so collapsed examples no longer stack it on the container border (thicker bottom edge). CLOUDP-399874 --- src/components/ipa/Example/Example.module.css | 15 +- .../ipa/Workflow/Workflow.module.css | 257 ++++++++++++++---- src/components/ipa/Workflow/Workflow.test.tsx | 29 +- src/components/ipa/Workflow/index.tsx | 72 ++++- 4 files changed, 305 insertions(+), 68 deletions(-) diff --git a/src/components/ipa/Example/Example.module.css b/src/components/ipa/Example/Example.module.css index 45535b1..13e2ff5 100644 --- a/src/components/ipa/Example/Example.module.css +++ b/src/components/ipa/Example/Example.module.css @@ -6,12 +6,15 @@ overflow: hidden; } -/* The toggle button for the Example accordion — coloured header bar */ +/* The toggle button for the Example accordion — coloured header bar. + The separator is transparent while collapsed so it doesn't stack on the + container's own bottom border; it only shows when content is open. */ .header { gap: 0.5rem; padding: 0.5rem 0.75rem; font-size: 0.875rem; font-weight: normal; + border-bottom: 1px solid transparent; } .icon { @@ -54,7 +57,10 @@ .correct .header { color: var(--ex-header-color); - border-bottom: 1px solid var(--ex-border); +} + +.correct .header[aria-expanded="true"] { + border-bottom-color: var(--ex-border); } .correct .header[aria-expanded="true"], @@ -80,7 +86,10 @@ .incorrect .header { color: var(--ex-header-color); - border-bottom: 1px solid var(--ex-border); +} + +.incorrect .header[aria-expanded="true"] { + border-bottom-color: var(--ex-border); } .incorrect .header[aria-expanded="true"], diff --git a/src/components/ipa/Workflow/Workflow.module.css b/src/components/ipa/Workflow/Workflow.module.css index 3276460..4c53079 100644 --- a/src/components/ipa/Workflow/Workflow.module.css +++ b/src/components/ipa/Workflow/Workflow.module.css @@ -1,113 +1,264 @@ -/* Neutral "process" treatment — emphasis grays, unlike Example's verdict colors */ +/* Neutral "process" treatment — Accordion's default chrome already fits, + only spacing and header density change */ .accordion { - border: none; - background: transparent; - border-radius: 0.5rem; - overflow: hidden; - - --wf-border: var(--ifm-color-emphasis-200); - --wf-bg: var(--ifm-color-emphasis-0); - --wf-header-color: var(--ifm-color-emphasis-700); - --wf-header-hover-bg: var(--ifm-color-emphasis-100); - - border: 1px solid var(--wf-border); - background-color: var(--wf-bg); + margin: 1.25rem 0 0.25rem; } .header { + padding: 0.7rem 0.75rem; +} + +.title { + flex-grow: 1; + display: flex; + align-items: center; gap: 0.5rem; - padding: 0.5rem 0.75rem; - font-size: 0.875rem; - font-weight: normal; - color: var(--wf-header-color); } -.header[aria-expanded="true"] { - border-bottom: 1px solid var(--wf-border); +.titleIcon { + flex-shrink: 0; + width: 1rem; + height: 1rem; + color: var(--ifm-color-secondary); } -.header[aria-expanded="true"], -.header:hover { - background-color: var(--wf-header-hover-bg) !important; +[data-theme="dark"] .titleIcon { + color: var(--ifm-color-primary); } -.title { +.titleText { + font-weight: 600; +} + +/* Progress chip — pill-shaped, right-aligned before the chevron */ +.progress { + margin-left: auto; display: inline-flex; - align-items: center; - gap: 0.5rem; } +.progress [data-color] { + border-radius: 9999px; + padding: 0.25rem 0.7rem; + font-size: 0.78rem; + transition: + background-color 0.25s ease, + border-color 0.25s ease, + color 0.25s ease; +} + +/* Panel owns no padding — rows and footer pad themselves so the + hairline separators span the full width */ .content { - padding: 0.75rem; + padding: 0 !important; border-top: none !important; } -.title :global(.badge) { - font-weight: normal; +/* Thin progress track between header and steps */ +.track { + height: 3px; + background: var(--ifm-color-emphasis-200); + overflow: hidden; } -.progress { - margin-left: auto; +.trackFill { + height: 100%; + background: linear-gradient( + 90deg, + var(--mongodb-sage-green), + var(--mongodb-green) + ); + border-radius: 0 2px 2px 0; + transition: width 0.3s ease; } -.steps { - margin: 0; - padding-left: 1.5rem; +.hint { + margin: 0.55rem 0.95rem 0.3rem; + font-size: 0.78rem; + color: var(--ifm-color-emphasis-600); } -.step { - margin-bottom: 0.4rem; +.steps { + list-style: none; + margin: 0; + padding: 0; } -.step:last-child { - margin-bottom: 0; +.step + .step .stepLabel { + border-top: 1px solid var(--ifm-color-emphasis-200); } .stepLabel { display: flex; - align-items: baseline; - gap: 0.5rem; + align-items: flex-start; + gap: 0.7rem; + padding: 0.7rem 0.95rem; cursor: pointer; + transition: background-color 0.15s ease; +} + +.stepLabel:hover { + background-color: var(--ifm-color-emphasis-100); } +[data-theme="dark"] .stepLabel:hover { + background-color: var(--ifm-color-emphasis-200); +} + +/* Real checkbox, visually replaced but still focusable */ .stepCheckbox { - flex-shrink: 0; + position: absolute; + opacity: 0; + width: 1px; + height: 1px; margin: 0; - accent-color: var(--ifm-color-primary-darkest); - cursor: pointer; + pointer-events: none; } -[data-theme="dark"] .stepCheckbox { - accent-color: var(--ifm-color-primary); +.stepBox { + flex-shrink: 0; + width: 1.15rem; + height: 1.15rem; + margin-top: 0.22rem; + border-radius: 0.3rem; + border: 1.5px solid var(--ifm-color-emphasis-500); + background: var(--ifm-background-surface-color); + display: inline-flex; + align-items: center; + justify-content: center; + transition: + background-color 0.15s ease, + border-color 0.15s ease; +} + +.stepBox svg { + width: 0.7rem; + height: 0.7rem; + stroke: var(--mongodb-slate); + stroke-width: 3; + fill: none; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 14; + stroke-dashoffset: 14; + transition: stroke-dashoffset 0.18s ease 0.04s; +} + +.stepLabel:hover .stepBox { + border-color: var(--mongodb-sage-green); +} + +.stepCheckbox:checked + .stepLabel .stepBox { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary-darkest); +} + +.stepCheckbox:checked + .stepLabel .stepBox svg { + stroke-dashoffset: 0; +} + +.stepCheckbox:focus-visible + .stepLabel { + outline: 2px solid var(--ifm-color-primary); + outline-offset: -2px; + border-radius: 2px; +} + +/* Step number circle — echoes the numbering motif, smaller + and muted so it doesn't compete with the guideline's own index */ +.stepNum { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + margin-top: 0.05rem; + border-radius: 9999px; + background: rgba(150, 150, 150, 0.1); + color: var(--ifm-color-emphasis-600); + font-size: 0.75rem; + font-weight: 600; + transition: + background-color 0.2s ease, + color 0.2s ease; +} + +.stepCheckbox:checked + .stepLabel .stepNum { + background: rgba(0, 237, 100, 0.12); + color: #007a35; +} + +[data-theme="dark"] .stepCheckbox:checked + .stepLabel .stepNum { + color: var(--ifm-color-primary); } /* Done treatment: mute, don't strike through — steps stay legible */ -.stepDone .stepText { +.stepText { + flex: 1; + min-width: 0; + font-size: 0.9rem; + line-height: 1.55; + transition: color 0.2s ease; +} + +.stepCheckbox:checked + .stepLabel .stepText { color: var(--ifm-color-emphasis-600); } +.stepText code { + font-size: 88%; +} + .footer { display: flex; + align-items: center; justify-content: flex-end; - margin-top: 0.75rem; + padding: 0.45rem 0.95rem 0.55rem; + border-top: 1px solid var(--ifm-color-emphasis-200); } .reset { border: none; - background: none; - padding: 0; - font-size: 0.8rem; + background: transparent; + font: inherit; + font-size: 0.78rem; + font-weight: 600; color: var(--ifm-color-emphasis-600); cursor: pointer; - transition: color 0.15s ease; + padding: 0.2rem 0.45rem; + border-radius: 0.3rem; + transition: + color 0.15s ease, + background-color 0.15s ease; } .reset:hover:not(:disabled) { - color: var(--ifm-font-color-base); - text-decoration: underline; + color: var(--ifm-color-secondary); + background: var(--ifm-color-emphasis-100); +} + +[data-theme="dark"] .reset:hover:not(:disabled) { + color: var(--ifm-color-primary); + background: var(--ifm-color-emphasis-200); } .reset:disabled { - opacity: 0.4; + opacity: 0.45; cursor: default; } + +.reset:focus-visible { + outline: 2px solid var(--ifm-color-primary); + outline-offset: 1px; +} + +@media (prefers-reduced-motion: reduce) { + .trackFill, + .stepBox, + .stepBox svg, + .stepLabel, + .stepNum, + .stepText, + .progress [data-color] { + transition: none; + } +} diff --git a/src/components/ipa/Workflow/Workflow.test.tsx b/src/components/ipa/Workflow/Workflow.test.tsx index d091a57..cf38514 100644 --- a/src/components/ipa/Workflow/Workflow.test.tsx +++ b/src/components/ipa/Workflow/Workflow.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, within } from "@testing-library/react"; import { describe, it, expect, vi, afterEach } from "vitest"; import { Workflow } from "./index"; @@ -67,6 +67,33 @@ describe("", () => { }); describe(" review checklist", () => { + it("renders the page-local progress hint", () => { + render( + + only step + , + ); + + expect( + screen.getByText(/progress is kept on this page only/i), + ).toBeInTheDocument(); + }); + + it("numbers each step with a decorative circle", () => { + render( + + first step + second step + , + ); + const list = screen.getByTestId("workflow-steps"); + + const one = within(list).getByText("1"); + const two = within(list).getByText("2"); + expect(one).toHaveAttribute("aria-hidden", "true"); + expect(two).toHaveAttribute("aria-hidden", "true"); + }); + it("renders an unchecked checkbox for each step", () => { render( diff --git a/src/components/ipa/Workflow/index.tsx b/src/components/ipa/Workflow/index.tsx index f80b4c3..bcc01cb 100644 --- a/src/components/ipa/Workflow/index.tsx +++ b/src/components/ipa/Workflow/index.tsx @@ -4,11 +4,11 @@ import { isValidElement, useContext, useEffect, + useId, useState, type ReactNode, type ReactElement, } from "react"; -import clsx from "clsx"; import { Accordion, Badge } from "../../ui"; import { GuidelineContext } from "../../../hooks/useGuideline"; import styles from "./Workflow.module.css"; @@ -21,24 +21,37 @@ interface WorkflowProps { interface WorkflowStepProps { children: ReactNode; // Injected by — not part of the authoring API. + index?: number; checked?: boolean; onToggle?: () => void; } function WorkflowStep({ children, + index = 1, checked = false, onToggle, }: WorkflowStepProps): ReactElement { + const checkboxId = useId(); + return ( -
  • -
  • + onToggle?.()} + /> +
  • @@ -81,7 +94,33 @@ function WorkflowBase({ title, children }: WorkflowProps): ReactElement { className={styles.accordion} title={ - {title ?? "Evaluation workflow"} + + + {title ?? "Evaluation workflow"} + {referenceOnly && ( Reference only — agent skips @@ -92,7 +131,7 @@ function WorkflowBase({ title, children }: WorkflowProps): ReactElement { data-testid="workflow-progress" aria-live="polite" > - + {done} of {total} verified @@ -101,10 +140,21 @@ function WorkflowBase({ title, children }: WorkflowProps): ReactElement { titleClassName={styles.header} contentClassName={styles.content} > +