import { playgroundParameters } from "@charcuterie/storybook-config/story-parameters" import type { Meta, StoryObj } from "@storybook/react" import { useState } from "react" import { ContainerBoard, StoryCell, StoryGrid, } from "../board.storyHelpers.tsx" import { Card } from "../Card/Card.tsx" import { MarkdownEditorCodeMirror } from "./MarkdownEditorCodeMirror.tsx" import { MarkdownView } from "./MarkdownView.tsx" /** * Invented, like every fixture in this repo — a task in a made-up * homelab project, and deliberately the **same string** the editor's * stories use. * * That is what makes the `SameAsTheEditor` story an argument rather * than an illustration: the two surfaces are handed one document and * a reader compares the frames. */ const SAMPLE = `# Rack move, phase two The **short** version: pull the _old_ switch, land the new one, and keep the ~~three~~ two uplinks live throughout. ## Before the window - [x] Label every patch lead - [ ] Photograph the current cabling - [ ] Cut over at 02:00 1. Power down the shelf 2. Swap the rails 3. Bring it back up > Nothing here touches the storage pool. Run \`ping -c 3 gateway.invalid\` before and after, and read [the runbook](https://example.invalid/runbook) first. | Port | Goes to | | --- | --- | | 1 | uplink | | 2 | shelf | \`\`\`sh ip -br addr show \`\`\` Replacement rails ordered from https://example.invalid/product?id=1234 ` /** * Link text with inline markup in it — the shape that used to take * the **whole document** down to raw source. * * The closing bracket was read as the node right after `[`, which * is the bracket only while the link text is plain prose. A code * span or a `**bold**` run becomes that node instead, the link text * measures zero characters, `Decoration.mark` refuses an empty * range, and the throw destroys the view plugin — so every * decoration in the file disappears at once, not just this link's. * * Invented, like every fixture here, but the shape is not invented: * agent-written task descriptions are full of ``[`file.md`](path)``. */ const NESTED_LINK_TEXT = `## Where the fix landed The range walk lives in [\`livePreviewRanges.ts\`](https://example.invalid/ranges) and the decorations in [\`livePreview.ts\`](https://example.invalid/preview). - A code span: [\`AGENTS.md\`](https://example.invalid/agents) - Strong: [**the runbook**](https://example.invalid/runbook) - Emphasis: [*the older note*](https://example.invalid/note) - Struck through: [~~the retired page~~](https://example.invalid/retired) - Mixed: [read \`this\` **first**](https://example.invalid/first) Plain links still work: [the index](https://example.invalid/index). ` /** * A drawing rather than a fetched file, so the story needs no * network and the frame is the same on every machine. The same * technique `Avatar`'s portrait uses, and one of the few things * `data:` is genuinely good for. */ const DIAGRAM_URL = `data:image/svg+xml;utf8,${encodeURIComponent( '', )}` const WITH_IMAGE = `## Rack inventory ![Two shelves and the uplink between them](${DIAGRAM_URL}) | Port | Goes to | **Speed** | | :--- | :---: | ---: | | 1 | uplink | 10G | | 2 | shelf | 1G | ` /** * The document nobody in this house wrote: what a fetched file looks * like when its author would rather it did something else. * * Every line here is a thing that used to work. The `javascript:` * link opened and ran; the ` ![Not an image](data:text/html,) [This one is fine](https://example.invalid/runbook), and so is ./RELEASING.md. ` const CHECKLIST = `## Before the window - [x] Label every patch lead - [ ] Photograph the current cabling - [ ] Cut over at 02:00 ` /** * A second list rather than the same one twice, and the difference * is load bearing: two checkboxes with the same accessible name on * one page are two things `getByRole("checkbox", { name })` cannot * tell apart — which is the drivability property this library gates * on, failing in a *story* rather than in a component. */ const TICKABLE_CHECKLIST = `## After the window - [x] Bring the shelf back up - [ ] Re-check both uplinks - [ ] Close the change record ` /** * The state the checkbox story cannot show without one: a tick that * goes somewhere. * * `onToggleTask` hands back the whole next markdown document, which * is the same shape the editors' `onChange` uses — so a consumer * wires one autosave and points both at it. */ const TickableChecklist = () => { const [value, setValue] = useState(TICKABLE_CHECKLIST) return ( ) } const meta = { title: "Components/Data/MarkdownView", component: MarkdownView, parameters: { layout: "padded" }, args: { label: "Description", onToggleTask: undefined, value: SAMPLE, }, } satisfies Meta export default meta type Story = StoryObj /** * The whole point in one frame: the document is drawn, and there is * **no toolbar above it**. Not a disabled one, not a hidden one — * `getAllByRole("toolbar")` returns nothing, because nothing in this * component can render one. */ export const Playground: Story = { parameters: playgroundParameters, } /** * The same string, in both surfaces, side by side. * * This is the requirement the component exists to meet, and it is * the one a screenshot can actually check: the heading is the same * size, the table has the same borders and the same alignment, the * checkboxes are in the same places, and the bare URL is a link in * both. They agree because they are the same code — one parser, one * set of range rules, one theme — rather than because somebody kept * two renderers in step. */ export const SameAsTheEditor: Story = { render: (viewProps) => ( ), } /** * Everything the reader can be shown, including the two states that * are easy to get wrong: an empty document, and a checkbox nobody * may tick. */ export const AllStates: Story = { render: (viewProps) => ( ), } /** * A file from a git host, rendering as the characters it contains. * * The `javascript:` link keeps its brackets and its parentheses on * purpose. A link-coloured word that silently refuses to work tells * the reader nothing; the source tells them exactly what the file * tried to do. */ export const HostileMarkdown: Story = { args: { label: "README.md at 9f3c1ab", value: HOSTILE, }, } /** * The three container widths. A table that will not fit scrolls * inside its own block rather than pushing the column sideways, and * an image is capped at the width it is given — which is what keeps * the Narrow View readable instead of merely small. */ export const Responsive: Story = { render: (viewProps) => ( {(width) => ( )} ), } /** * The complete keyboard path, which for a reader is short and is the * point. * * The document itself is **not** a tab stop — there is nothing in it * to operate, and a reading surface that swallowed a Tab would be * one more thing between the reader and the next control. What *is* * reachable is every link in it, in document order, because they are * real anchors rather than painted spans: Tab to one, Enter to open * it, and the context menu will copy its address. * * The buttons on either side are there so the path is visible — Tab * from the first lands on each link in turn and ends on the second. */ export const Interactive: Story = { args: { label: "Rack move, phase two", value: SAMPLE, }, render: (viewProps) => (
), } /** * The regression frame. * * Every line here is a link whose text carries inline markup, and * the assertion a reader can make from the picture is the one that * matters: the document is **drawn**. Before the fix this frame was * the markdown source, brackets and parentheses and all, because * one empty mark decoration killed the plugin for the whole file. * * The last line is the control. A plain `[text](url)` never had the * bug, so if it renders and the ones above it do not, the failure * is this and not something else. */ export const NestedLinkText: Story = { args: { label: "Where the fix landed", value: NESTED_LINK_TEXT, }, }