import React from 'react'; import type { AlertKind } from '../../types'; import { renderProseBody } from './proseBody'; import { InlineMarkdown } from '../InlineMarkdown'; import { parseAlertTitleLine } from '../../utils/alertTitle'; interface AlertBlockProps { blockId: string; kind: AlertKind; body: string; onOpenLinkedDoc?: (path: string) => void; onOpenCodeFile?: (path: string) => void; imageBaseDir?: string; onImageClick?: (src: string, alt: string) => void; githubRepo?: string; repoHost?: string; onNavigateAnchor?: (hash: string) => void; } const TITLE: Record = { note: 'Note', tip: 'Tip', warning: 'Warning', caution: 'Caution', important: 'Important', }; /** * Host seam: resolve a named icon from a title line's `` * comment to a React node. Default: null for every name, so the alert keeps * the type's own icon exactly as today. A host that ships an icon set (the * package deliberately bundles none) registers a renderer once at startup. * Returning null for an unknown name falls back to the type's icon. */ export type AlertIconRenderer = (name: string) => React.ReactNode | null; let alertIconRenderer: AlertIconRenderer | null = null; export function setAlertIconRenderer(renderer: AlertIconRenderer | null): void { alertIconRenderer = renderer; } export function resetAlertIconRenderer(): void { alertIconRenderer = null; } const Icon: React.FC<{ kind: AlertKind }> = ({ kind }) => { const common = { viewBox: '0 0 16 16', width: '16', height: '16', fill: 'currentColor', 'aria-hidden': true as const }; switch (kind) { case 'note': return ; case 'tip': return ; case 'warning': return ; case 'caution': return ; case 'important': return ; } }; export const AlertBlock: React.FC = ({ blockId, kind, body, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, githubRepo, repoHost, onNavigateAnchor, }) => { const proseProps = { imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor, githubRepo, repoHost }; // A bold-only first line (optionally led by one emoji, optionally trailed by // an `` comment) is the alert's title and rides the icon // row in place of the type word; the emoji, or a host-resolved named icon, // takes the icon slot. See utils/alertTitle for the exact grammar. A body // with no such line renders exactly as before. const titleLine = parseAlertTitleLine(body); // The emoji wins outright: the host renderer is consulted only for a title // line that carries an icon comment and no emoji (the seam contract). const icon = titleLine?.emoji ? : (titleLine?.icon && alertIconRenderer ? alertIconRenderer(titleLine.icon) : null) ?? ; return (
{icon} {titleLine?.title ? ( <> {/* The type word stays part of the accessible name. A visually hidden span is read by every engine; an aria-label on this generic div is prohibited by ARIA and dropped by WebKit. `annotation-exclude` keeps it out of the annotation text stream: without it a drag that starts on the icon quotes the invisible word ("Warning:Named icon title"), which is what the panel shows, what the agent is handed, and a quote no share-link restore can find again. */} {TITLE[kind]}: ) : TITLE[kind]}
{titleLine ? (titleLine.rest.trim() // Indent the body by the icon's width plus the row gap so it starts // at the title's left edge (16px icon + 8px gap = pl-6). ?
{renderProseBody({ body: titleLine.rest, ...proseProps })}
: null) : renderProseBody({ body, ...proseProps })}
); };