From fb4b68345b4f3f68eb55b45ba940290cc2a90f9a Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Fri, 12 Jun 2026 11:21:48 +0100 Subject: [PATCH 1/3] feat(ipa): Add explanation component Renders the why-explanation inside / blocks: beneath the code block, separated by the example's tinted hairline, with an inline bold "Why:" lead-in in the verdict color. Falls back to neutral tokens when rendered outside an example. CLOUDP-399873 --- ipa/dev/component-fixtures.mdx | 51 +++++++++++++++++++++ src/components/ipa/Reason/Reason.module.css | 33 +++++++++++++ src/components/ipa/Reason/Reason.test.tsx | 39 ++++++++++++++++ src/components/ipa/Reason/index.tsx | 14 ++++++ src/components/ipa/index.ts | 1 + 5 files changed, 138 insertions(+) create mode 100644 src/components/ipa/Reason/Reason.module.css create mode 100644 src/components/ipa/Reason/Reason.test.tsx create mode 100644 src/components/ipa/Reason/index.tsx diff --git a/ipa/dev/component-fixtures.mdx b/ipa/dev/component-fixtures.mdx index 5c0c6bd..3d4aa06 100644 --- a/ipa/dev/component-fixtures.mdx +++ b/ipa/dev/component-fixtures.mdx @@ -13,6 +13,7 @@ import { Guidelines, Guideline, Example, + Reason, Workflow, } from "@site/src/components/ipa"; @@ -119,6 +120,56 @@ name: list-resources +## `` + +`` is the short prose explanation inside `` and +`` blocks — why the pattern is correct or incorrect, the +principle rather than a restatement of the guideline. It renders beneath the +code block under the example's tinted hairline, with an inline "Why:" lead-in in +the verdict color. + +### Inside a correct example + + + +```yaml +PATCH /clusters/{clusterId} +Content-Type: application/merge-patch+json +``` + + + Merge-patch is simpler for clients — send only the fields to change. + + + + +### Inside an incorrect example, long-form + + + +```yaml +components: + schemas: + Cluster: + properties: + _id: { type: string } + __v: { type: integer } +``` + + + Exposing `_id` and `__v` leaks MongoDB document internals into the API + surface, coupling clients to the storage layer. + + + + +### Outside an example (fallback styling) + +A `` rendered outside an example falls back to neutral colors — the +hairline uses the default border token and the lead inherits the text color. + +Rendered standalone, no verdict tint is available. + ## `` `` renders the manual evaluation steps of a guideline as a numbered diff --git a/src/components/ipa/Reason/Reason.module.css b/src/components/ipa/Reason/Reason.module.css new file mode 100644 index 0000000..7fd1f42 --- /dev/null +++ b/src/components/ipa/Reason/Reason.module.css @@ -0,0 +1,33 @@ +/* Sits beneath the code block inside an Example's content area, separated + by the example's own tinted hairline. The --ex-* tokens are defined on the + Example variant root and cascade down; the fallbacks keep the component + legible if it is ever rendered outside an Example. */ +.reason { + margin-top: 0.75rem; + padding-top: 0.6rem; + border-top: 1px solid var(--ex-border, var(--ifm-color-emphasis-200)); + font-size: 0.85rem; + line-height: 1.6; + color: var(--ifm-font-color-base); +} + +.lead { + font-weight: 700; + color: var(--ex-header-color, inherit); +} + +.reason code { + font-size: 88%; +} + +/* MDX wraps indented children in

. The first paragraph flows inline after + the "Why:" lead; any further paragraphs break below with a small gap. */ +.reason p { + display: inline; + margin: 0; +} + +.reason p + p { + display: block; + margin-top: 0.5rem; +} diff --git a/src/components/ipa/Reason/Reason.test.tsx b/src/components/ipa/Reason/Reason.test.tsx new file mode 100644 index 0000000..904cb28 --- /dev/null +++ b/src/components/ipa/Reason/Reason.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; + +import { Reason } from "./index"; + +describe("", () => { + it("renders the explanation prose", () => { + render(Merge-patch is simpler for clients.); + + expect( + screen.getByText(/merge-patch is simpler for clients/i), + ).toBeInTheDocument(); + }); + + it("renders a 'Why:' lead-in", () => { + render(Some explanation.); + + expect(screen.getByText("Why:")).toBeInTheDocument(); + }); + + it("accepts paragraph children without invalid DOM nesting", () => { + // MDX wraps indented multi-line children in

, so the component's + // container must not itself be a

(React warns via console.error). + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + + render( + +

First sentence of the reason.

+
, + ); + + expect( + screen.getByText(/first sentence of the reason/i), + ).toBeInTheDocument(); + expect(error).not.toHaveBeenCalled(); + + error.mockRestore(); + }); +}); diff --git a/src/components/ipa/Reason/index.tsx b/src/components/ipa/Reason/index.tsx new file mode 100644 index 0000000..d6ef699 --- /dev/null +++ b/src/components/ipa/Reason/index.tsx @@ -0,0 +1,14 @@ +import { type ReactElement, type ReactNode } from "react"; +import styles from "./Reason.module.css"; + +interface ReasonProps { + children: ReactNode; +} + +export function Reason({ children }: ReasonProps): ReactElement { + return ( +
+ Why: {children} +
+ ); +} diff --git a/src/components/ipa/index.ts b/src/components/ipa/index.ts index e2e30a4..5ebc004 100644 --- a/src/components/ipa/index.ts +++ b/src/components/ipa/index.ts @@ -2,4 +2,5 @@ export { Guidelines } from "./Guidelines"; export { Guideline } from "./Guideline"; export { PrincipleHeader } from "./PrincipleHeader"; export { Example } from "./Example"; +export { Reason } from "./Reason"; export { Workflow } from "./Workflow"; From 9c9b2d77ea7f8cddb16af56144ab62b7766ad6ed Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Fri, 12 Jun 2026 16:46:16 +0100 Subject: [PATCH 2/3] refactor(ipa): Namespace Reason as Example.Reason MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Examples are authored as /, so the explanation inside them joins the same compound family (matching the Workflow.Step convention). The component moves into the Example folder and the top-level Reason export is removed — one way to author it. Rendering is unchanged. CLOUDP-399873 --- ipa/dev/component-fixtures.mdx | 20 ++++--------- .../ipa/{Reason => Example}/Reason.module.css | 0 .../ipa/{Reason => Example}/Reason.test.tsx | 28 +++++++++++++++---- .../{Reason/index.tsx => Example/Reason.tsx} | 0 src/components/ipa/Example/index.tsx | 2 ++ src/components/ipa/index.ts | 1 - 6 files changed, 30 insertions(+), 21 deletions(-) rename src/components/ipa/{Reason => Example}/Reason.module.css (100%) rename src/components/ipa/{Reason => Example}/Reason.test.tsx (56%) rename src/components/ipa/{Reason/index.tsx => Example/Reason.tsx} (100%) diff --git a/ipa/dev/component-fixtures.mdx b/ipa/dev/component-fixtures.mdx index 3d4aa06..adee63a 100644 --- a/ipa/dev/component-fixtures.mdx +++ b/ipa/dev/component-fixtures.mdx @@ -13,7 +13,6 @@ import { Guidelines, Guideline, Example, - Reason, Workflow, } from "@site/src/components/ipa"; @@ -120,9 +119,9 @@ name: list-resources -## `` +## `` -`` is the short prose explanation inside `` and +`` is the short prose explanation inside `` and `` blocks — why the pattern is correct or incorrect, the principle rather than a restatement of the guideline. It renders beneath the code block under the example's tinted hairline, with an inline "Why:" lead-in in @@ -137,9 +136,9 @@ PATCH /clusters/{clusterId} Content-Type: application/merge-patch+json ``` - + Merge-patch is simpler for clients — send only the fields to change. - + @@ -156,20 +155,13 @@ components: __v: { type: integer } ``` - + Exposing `_id` and `__v` leaks MongoDB document internals into the API surface, coupling clients to the storage layer. - +
-### Outside an example (fallback styling) - -A `` rendered outside an example falls back to neutral colors — the -hairline uses the default border token and the lead inherits the text color. - -Rendered standalone, no verdict tint is available. - ## `` `` renders the manual evaluation steps of a guideline as a numbered diff --git a/src/components/ipa/Reason/Reason.module.css b/src/components/ipa/Example/Reason.module.css similarity index 100% rename from src/components/ipa/Reason/Reason.module.css rename to src/components/ipa/Example/Reason.module.css diff --git a/src/components/ipa/Reason/Reason.test.tsx b/src/components/ipa/Example/Reason.test.tsx similarity index 56% rename from src/components/ipa/Reason/Reason.test.tsx rename to src/components/ipa/Example/Reason.test.tsx index 904cb28..5282c45 100644 --- a/src/components/ipa/Reason/Reason.test.tsx +++ b/src/components/ipa/Example/Reason.test.tsx @@ -1,11 +1,13 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; -import { Reason } from "./index"; +import { Example } from "./index"; -describe("", () => { +describe("", () => { it("renders the explanation prose", () => { - render(Merge-patch is simpler for clients.); + render( + Merge-patch is simpler for clients., + ); expect( screen.getByText(/merge-patch is simpler for clients/i), @@ -13,7 +15,7 @@ describe("", () => { }); it("renders a 'Why:' lead-in", () => { - render(Some explanation.); + render(Some explanation.); expect(screen.getByText("Why:")).toBeInTheDocument(); }); @@ -24,9 +26,9 @@ describe("", () => { const error = vi.spyOn(console, "error").mockImplementation(() => {}); render( - +

First sentence of the reason.

-
, +
, ); expect( @@ -36,4 +38,18 @@ describe("", () => { error.mockRestore(); }); + + it("renders inside an example beneath its code block", () => { + render( + +
+          name: list-resources
+        
+ The name field is required. +
, + ); + + expect(screen.getByText(/the name field is required/i)).toBeInTheDocument(); + expect(screen.getByText("Why:")).toBeInTheDocument(); + }); }); diff --git a/src/components/ipa/Reason/index.tsx b/src/components/ipa/Example/Reason.tsx similarity index 100% rename from src/components/ipa/Reason/index.tsx rename to src/components/ipa/Example/Reason.tsx diff --git a/src/components/ipa/Example/index.tsx b/src/components/ipa/Example/index.tsx index 30e9a10..b673621 100644 --- a/src/components/ipa/Example/index.tsx +++ b/src/components/ipa/Example/index.tsx @@ -1,6 +1,7 @@ import { type ReactElement, type ReactNode } from "react"; import { Accordion } from "@site/src/components/ui"; import { type ExampleType } from "./types"; +import { Reason } from "./Reason"; import styles from "./Example.module.css"; import clsx from "clsx"; @@ -47,4 +48,5 @@ IncorrectExample.displayName = "Incorrect"; export const Example = Object.assign(ExampleBase, { Correct: CorrectExample, Incorrect: IncorrectExample, + Reason: Reason, }); diff --git a/src/components/ipa/index.ts b/src/components/ipa/index.ts index 5ebc004..e2e30a4 100644 --- a/src/components/ipa/index.ts +++ b/src/components/ipa/index.ts @@ -2,5 +2,4 @@ export { Guidelines } from "./Guidelines"; export { Guideline } from "./Guideline"; export { PrincipleHeader } from "./PrincipleHeader"; export { Example } from "./Example"; -export { Reason } from "./Reason"; export { Workflow } from "./Workflow"; From 427aa7be763812a57bc5a7152c86a4876c247d41 Mon Sep 17 00:00:00 2001 From: Andrei Matei Date: Fri, 12 Jun 2026 17:15:53 +0100 Subject: [PATCH 3/3] refactor(ipa): Drop dead CSS fallbacks and tighten Reason comments Example.Reason is only reachable through the Example namespace, so the outside-an-Example fallback tokens defended a case the API no longer invites. The position test now asserts the reason follows the code block instead of only checking presence. --- src/components/ipa/Example/Reason.module.css | 14 ++++++-------- src/components/ipa/Example/Reason.test.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/ipa/Example/Reason.module.css b/src/components/ipa/Example/Reason.module.css index 7fd1f42..d8abdfb 100644 --- a/src/components/ipa/Example/Reason.module.css +++ b/src/components/ipa/Example/Reason.module.css @@ -1,11 +1,9 @@ -/* Sits beneath the code block inside an Example's content area, separated - by the example's own tinted hairline. The --ex-* tokens are defined on the - Example variant root and cascade down; the fallbacks keep the component - legible if it is ever rendered outside an Example. */ +/* The --ex-* tokens are defined on the Example variant root + (Example.module.css) and reach these rules by cascade. */ .reason { margin-top: 0.75rem; padding-top: 0.6rem; - border-top: 1px solid var(--ex-border, var(--ifm-color-emphasis-200)); + border-top: 1px solid var(--ex-border); font-size: 0.85rem; line-height: 1.6; color: var(--ifm-font-color-base); @@ -13,15 +11,15 @@ .lead { font-weight: 700; - color: var(--ex-header-color, inherit); + color: var(--ex-header-color); } .reason code { font-size: 88%; } -/* MDX wraps indented children in

. The first paragraph flows inline after - the "Why:" lead; any further paragraphs break below with a small gap. */ +/* MDX wraps the prose in

: the first paragraph flows inline after the + "Why:" lead, subsequent paragraphs break below. */ .reason p { display: inline; margin: 0; diff --git a/src/components/ipa/Example/Reason.test.tsx b/src/components/ipa/Example/Reason.test.tsx index 5282c45..0aa4e27 100644 --- a/src/components/ipa/Example/Reason.test.tsx +++ b/src/components/ipa/Example/Reason.test.tsx @@ -48,8 +48,11 @@ describe("", () => { The name field is required. , ); + const code = screen.getByText("name: list-resources"); + const reason = screen.getByText(/the name field is required/i); - expect(screen.getByText(/the name field is required/i)).toBeInTheDocument(); - expect(screen.getByText("Why:")).toBeInTheDocument(); + expect( + code.compareDocumentPosition(reason) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); }); });