Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions ipa/dev/component-fixtures.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,49 @@ name: list-resources

</Example.Incorrect>

## `<Example.Reason>`

`<Example.Reason>` is the short prose explanation inside `<Example.Correct>` and
`<Example.Incorrect>` 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

<Example.Correct>

```yaml
PATCH /clusters/{clusterId}
Content-Type: application/merge-patch+json
```

<Example.Reason>
Merge-patch is simpler for clients — send only the fields to change.
</Example.Reason>

</Example.Correct>

### Inside an incorrect example, long-form

<Example.Incorrect>

```yaml
components:
schemas:
Cluster:
properties:
_id: { type: string }
__v: { type: integer }
```

<Example.Reason>
Exposing `_id` and `__v` leaks MongoDB document internals into the API
surface, coupling clients to the storage layer.
</Example.Reason>

</Example.Incorrect>

## `<Workflow>`

`<Workflow>` renders the manual evaluation steps of a guideline as a numbered
Expand Down
31 changes: 31 additions & 0 deletions src/components/ipa/Example/Reason.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* 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);
font-size: 0.85rem;
line-height: 1.6;
color: var(--ifm-font-color-base);
}

.lead {
font-weight: 700;
color: var(--ex-header-color);
}

.reason code {
font-size: 88%;
}

/* MDX wraps the prose in <p>: the first paragraph flows inline after the
"Why:" lead, subsequent paragraphs break below. */
.reason p {
display: inline;
margin: 0;
}

.reason p + p {
display: block;
margin-top: 0.5rem;
}
58 changes: 58 additions & 0 deletions src/components/ipa/Example/Reason.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";

import { Example } from "./index";

describe("<Example.Reason>", () => {
it("renders the explanation prose", () => {
render(
<Example.Reason>Merge-patch is simpler for clients.</Example.Reason>,
);

expect(
screen.getByText(/merge-patch is simpler for clients/i),
).toBeInTheDocument();
});

it("renders a 'Why:' lead-in", () => {
render(<Example.Reason>Some explanation.</Example.Reason>);

expect(screen.getByText("Why:")).toBeInTheDocument();
});

it("accepts paragraph children without invalid DOM nesting", () => {
// MDX wraps indented multi-line children in <p>, so the component's
// container must not itself be a <p> (React warns via console.error).
const error = vi.spyOn(console, "error").mockImplementation(() => {});

render(
<Example.Reason>
<p>First sentence of the reason.</p>
</Example.Reason>,
);

expect(
screen.getByText(/first sentence of the reason/i),
).toBeInTheDocument();
expect(error).not.toHaveBeenCalled();

error.mockRestore();
});

it("renders inside an example beneath its code block", () => {
render(
<Example.Correct>
<pre>
<code>name: list-resources</code>
</pre>
<Example.Reason>The name field is required.</Example.Reason>
</Example.Correct>,
);
const code = screen.getByText("name: list-resources");
const reason = screen.getByText(/the name field is required/i);

expect(
code.compareDocumentPosition(reason) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
});
});
14 changes: 14 additions & 0 deletions src/components/ipa/Example/Reason.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.reason}>
<span className={styles.lead}>Why:</span> {children}
</div>
);
}
2 changes: 2 additions & 0 deletions src/components/ipa/Example/index.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -47,4 +48,5 @@ IncorrectExample.displayName = "Incorrect";
export const Example = Object.assign(ExampleBase, {
Correct: CorrectExample,
Incorrect: IncorrectExample,
Reason: Reason,
});