prose: the key-idea callout — GitHub alerts, one sharp white box; tables hug
The one emphasis device for load-bearing ideas: GitHub-style alert callouts. The prompt teaches only > [!IMPORTANT] (one device, one meaning — an unconditional truth laid down, a payoff landed, a hinge definition) with the restraint law: everything else stays in prose, if everything is boxed nothing is. The renderer accepts all five GitHub markers and boxes them identically — a stray [!NOTE] still meant 'this deserves emphasis', and a marker line must never leak as text. Rendering (owner rulings): SHARP corners, white stroke, no background, no label, no icon — just the outline — and the box hugs its text (width: fit-content capped at the column) instead of stretching full-width into empty space. Same grammar as the plan blocks: this is structure, not chrome. Seam: the Streamdown blockquote override in md-blocks. parseAlertMarker is pure and exported — Streamdown wraps blocks in its own styled components, so detection reads the hast node's tagName, and a standalone marker drops its <br> hard-break artifact along with the empty paragraph. Plain blockquotes pass through untouched. Rider (owner, same round): tables hug their contents — width: max-content on the data-streamdown table (width:auto would still stretch, the classic table trap); wide ones keep the md-blocks overflow-x scroller. Graceful in exports: a callout is still a blockquote there. 6 render pins: the box mounts with white stroke + fit-content + no rounding, all five markers box, standalone-marker drops the empty paragraph, multi-paragraph structure kept, plain quotes untouched.
This commit is contained in:
@@ -353,6 +353,12 @@ body {
|
||||
no edge doubles. (md-blocks supplies the wrapper + the copy corner.) */
|
||||
[data-streamdown="table"] {
|
||||
border: 1px solid var(--border);
|
||||
/* Tables HUG their contents (owner, callout round 2026-10-07): no more
|
||||
full-width stretch with dead space. max-content sizes to the cells
|
||||
(width:auto would still stretch — the classic table trap). No
|
||||
max-width cap: a max-width would squish-wrap wide tables instead of
|
||||
letting the md-blocks overflow-x scroller carry them. */
|
||||
width: max-content;
|
||||
/* Tables run at the SESSION prose size (owner note, 2026-09-19: they
|
||||
visibly lagged the 16px chat text at streamdown's 12.5px text-sm).
|
||||
Unlayered CSS beats the cells' own utility-layer text-sm, and the
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* EXPAND icon (opens the session overlay) when a chat page is mounted.
|
||||
*/
|
||||
|
||||
import { isValidElement, useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react";
|
||||
import { cloneElement, isValidElement, useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react";
|
||||
import { CheckIcon, CopyIcon, MaximizeIcon } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
@@ -265,6 +265,88 @@ export function MarkdownCode(props: ComponentProps<"code"> & ExtraProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Key-idea callouts (the Streamdown `blockquote` override) ────────────────
|
||||
|
||||
/** GitHub-style alert markers — the syntax the models already know from a
|
||||
* million READMEs. The PROMPT teaches only [!IMPORTANT] (one emphasis
|
||||
* device, one meaning: load-bearing); the renderer accepts all five and
|
||||
* boxes them identically — a stray [!NOTE] still meant "this deserves
|
||||
* emphasis", and a literal marker line must never leak as text (owner
|
||||
* design, callout round 2026-10-07). */
|
||||
const ALERT_MARKER_RE = /^\[!(note|tip|important|warning|caution)\][ \t]*(?:\n|$)/i;
|
||||
|
||||
export interface ParsedAlert {
|
||||
kind: string;
|
||||
/** The blockquote children with the marker line stripped — prose intact. */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const significant = (c: ReactNode) => c !== null && c !== undefined && c !== "" && !(typeof c === "string" && c.trim() === "");
|
||||
|
||||
/** The element's markdown tag — Streamdown wraps every block in its own
|
||||
* styled component (MarkdownParagraph, …), so the React type is rarely
|
||||
* the raw "p": the hast node carried in props tells the truth. */
|
||||
function markdownTag(el: React.ReactElement): string | null {
|
||||
if (typeof el.type === "string") return el.type;
|
||||
const node = (el.props as { node?: { tagName?: string } }).node;
|
||||
return node?.tagName ?? null;
|
||||
}
|
||||
|
||||
/** Pure: does this blockquote open with a GitHub alert marker? Returns the
|
||||
* kind and the children with the marker line removed (the marker's own
|
||||
* paragraph dropped entirely when it stands alone — `> [!IMPORTANT]` then
|
||||
* a blank quote line then content; a standalone marker also drops the
|
||||
* <br> hard-break artifact that carried its line break). Non-alert
|
||||
* blockquotes pass through untouched. Exported for hermetic tests. */
|
||||
export function parseAlertMarker(children: ReactNode): ParsedAlert | null {
|
||||
const list = Array.isArray(children) ? children : [children];
|
||||
const para = list.find(significant);
|
||||
if (!isValidElement(para) || markdownTag(para) !== "p") return null;
|
||||
const inner = (para.props as { children?: ReactNode }).children;
|
||||
const innerList = Array.isArray(inner) ? inner : [inner];
|
||||
const lead = innerList.find(significant);
|
||||
if (typeof lead !== "string") return null;
|
||||
const match = ALERT_MARKER_RE.exec(lead);
|
||||
if (!match) return null;
|
||||
const kind = match[1]!.toLowerCase();
|
||||
const stripped = lead.slice(match[0].length);
|
||||
// Rebuild the paragraph without the marker line; drop it entirely when
|
||||
// the marker stood alone (nothing but the marker text in the paragraph).
|
||||
const leadIdxInner = innerList.indexOf(lead);
|
||||
const hasFollowing = innerList.some((c, i) => i > leadIdxInner && significant(c));
|
||||
let rebuiltPara: ReactNode = null;
|
||||
if (stripped.trim() !== "" || hasFollowing) {
|
||||
let rest = innerList.map((c) => (c === lead ? (stripped === "" ? null : stripped) : c));
|
||||
if (stripped === "") {
|
||||
// the hard-break <br> that ended the marker line is now leading junk
|
||||
while (rest.length && (rest[0] === null || (isValidElement(rest[0]) && markdownTag(rest[0]) === "br"))) rest = rest.slice(1);
|
||||
}
|
||||
rebuiltPara = cloneElement(para, {}, ...rest);
|
||||
}
|
||||
const leadIdx = list.indexOf(para);
|
||||
const restChildren = [...list.slice(0, leadIdx), ...(rebuiltPara ? [rebuiltPara] : []), ...list.slice(leadIdx + 1)];
|
||||
return { kind, children: restChildren };
|
||||
}
|
||||
|
||||
/** The key-idea callout (owner rulings, 2026-10-07): SHARP corners, a white
|
||||
* stroke, NO background, NO label, NO icon — just the outline. The box
|
||||
* hugs its text (width: fit-content, capped at the column) instead of
|
||||
* stretching full-width into empty space. */
|
||||
export function MarkdownBlockquote(props: ComponentProps<"blockquote"> & ExtraProps) {
|
||||
const { children, node, ...rest } = props;
|
||||
const alert = parseAlertMarker(children);
|
||||
if (!alert) return <blockquote {...rest}>{children}</blockquote>;
|
||||
return (
|
||||
<div
|
||||
data-testid="md-callout"
|
||||
data-alert-kind={alert.kind}
|
||||
className="my-4 w-fit max-w-full border border-white px-4 py-3 [&> :first-child]:mt-0 [&> :last-child]:mb-0"
|
||||
>
|
||||
{alert.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Tables (the Streamdown `table` override) ────────────────────────────────
|
||||
|
||||
/** A plain PROSE table: no wrapper box, no button rows — just the table
|
||||
@@ -367,4 +449,5 @@ export const mdComponents = {
|
||||
table: MarkdownTable,
|
||||
span: MathDisplay,
|
||||
strong: MarkdownStrong,
|
||||
blockquote: MarkdownBlockquote,
|
||||
};
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
Everything you write — every message, every quiz question, every quiz option and explanation — renders as markdown to him, and the full set is available: headings, lists, tables, fenced code, inline code, bold, mermaid diagrams. Use it maximally when it fits — the default is a structured document, not a wall of prose. Headings for the sections a message naturally has; lists and tables for anything enumerable or comparative; code blocks for anything literal. Structure is not decoration — it makes the content readable at a glance — and when in doubt, structure it.
|
||||
|
||||
- Math: anything expressible in LaTeX goes in LaTeX — formulas, symbols, even single variables — everywhere text renders, and that means EVERY field: prose, quiz questions, quiz options, explanations, problem statements, answers. Inline `\(f(x)\)`, display `\[` fenced on its own lines. NEVER `$` or `$$` — a bare `$` is literal currency here; `\(...\)` and `\[...\]` are the only delimiters that render.
|
||||
- Key ideas: a GitHub-style `> [!IMPORTANT]` callout renders as a distinct outlined block — the one emphasis device for load-bearing ideas. Use it sparingly: an unconditional truth as it's laid down, a payoff when it lands, a definition the whole chain hinges on. Everything else stays in prose — if everything is boxed, nothing is.
|
||||
- Diagrams: ```mermaid fenced blocks render as real diagrams — reach for one whenever structure, flow, or relationships are the point; a good diagram beats three paragraphs. Node labels with spaces or symbols must use quoted bracket form (`B["u = x^3"]`).
|
||||
|
||||
@@ -173,3 +173,59 @@ describe("mermaid expand — the context seam", () => {
|
||||
assert.deepEqual(opened, [], "nothing opened by rendering alone");
|
||||
});
|
||||
});
|
||||
|
||||
describe("md-blocks — key-idea callouts (GitHub alerts, owner round 2026-10-07)", () => {
|
||||
it("an [!IMPORTANT] callout renders as the sharp white-outline box, marker never leaks", () => {
|
||||
const html = render("> [!IMPORTANT]\n> ALL X is done through Y.");
|
||||
assert.ok(html.includes('data-testid="md-callout"'), "the callout box mounts");
|
||||
assert.ok(html.includes('data-alert-kind="important"'), "the kind is recorded");
|
||||
assert.match(html, /ALL X is done through Y\./, "the prose survives");
|
||||
assert.ok(!html.includes("[!IMPORTANT]"), "the marker line never leaks as text");
|
||||
// The owner's minimal law: sharp corners (no rounded), white stroke,
|
||||
// no background, no label, no icon — and the box hugs its text.
|
||||
assert.match(html, /border-white/, "white stroke");
|
||||
assert.match(html, /w-fit/, "width: fit-content — the hug");
|
||||
assert.ok(!html.includes("rounded"), "sharp corners — no rounding anywhere");
|
||||
assert.ok(!html.includes("bg-card") && !html.includes("bg-background"), "transparent — no background");
|
||||
});
|
||||
|
||||
it("all five GitHub markers box identically — one device, one meaning", () => {
|
||||
for (const kind of ["NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"]) {
|
||||
const html = render(`> [!${kind}]\n> Load-bearing words.`);
|
||||
assert.ok(html.includes('data-testid="md-callout"'), `${kind} boxes`);
|
||||
assert.ok(html.includes(`data-alert-kind="${kind.toLowerCase()}"`), `${kind} kind recorded`);
|
||||
assert.ok(!html.includes(`[!${kind}]`), `${kind} marker never leaks`);
|
||||
}
|
||||
});
|
||||
|
||||
it("a standalone-marker callout (blank quote line) drops the empty paragraph", () => {
|
||||
const html = render("> [!IMPORTANT]\n>\n> The payoff lands here.");
|
||||
assert.ok(html.includes('data-testid="md-callout"'));
|
||||
assert.match(html, /The payoff lands here\./);
|
||||
assert.ok(!html.includes("[!IMPORTANT]"));
|
||||
});
|
||||
|
||||
it("multi-paragraph callouts keep their structure", () => {
|
||||
const html = render("> [!IMPORTANT]\n> First the truth.\n>\n> Then the payoff.");
|
||||
assert.match(html, /First the truth\./);
|
||||
assert.match(html, /Then the payoff\./);
|
||||
assert.ok((html.match(/<p>/g) ?? []).length >= 2, "both paragraphs render inside the box");
|
||||
});
|
||||
|
||||
it("plain blockquotes pass through untouched — no box for ordinary quotes", () => {
|
||||
const html = render("> Just an ordinary quote.");
|
||||
assert.ok(!html.includes('data-testid="md-callout"'), "no callout box");
|
||||
assert.match(html, /Just an ordinary quote\./);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseAlertMarker (pure)", () => {
|
||||
it("non-paragraph openers and marker-less text return null", async () => {
|
||||
const { parseAlertMarker } = await import("../components/md-blocks.tsx");
|
||||
assert.equal(parseAlertMarker("just text"), null);
|
||||
assert.equal(parseAlertMarker(null), null);
|
||||
// a leading <em> node, not a paragraph: no crash, no alert
|
||||
const em = { type: "em", props: { children: "x" } } as never;
|
||||
assert.equal(parseAlertMarker(em), null);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user