vitrine

callouts: restyle boxes, revise type vocabulary, normalize warn

commit 04acf71067b695d4b8b073103377d9bfaf26f105 · parent 40c7f54f5b · browse this tree
Ximianth, 2026-08-09 11:09:37 UTC

Restyle: each type drives one --callout hue, and the box derives a
tinted fill and matching border from it via color-mix, plus a soft drop
shadow and vertical margin.  Titles keep the caps/letter-spacing look
and gain an emoji icon via --callout-icon (following the heading
chainlink's emoji-as-icon idiom); only known types set an icon, so an
unknown type stays a clean icon-less neutral box.

Vocabulary is now info, note, tip, todo, warning, danger; info and note
ride the per-host accent.  warn is normalized to warning in the parser
via a small alias table, kept as a table so the mapping is cheap to
grow, rather than duplicating spellings across the CSS.
diff --git a/README.md b/README.md
index f5c1a9c..ea82de0 100644
--- a/README.md
+++ b/README.md
@@ -84,8 +84,9 @@ body content, which is **full markdown**
 The rendered box is `<div class="callout callout-{type}">`
 (matching the markdown-it-container / VuePress convention,
 so docs authored for those tools render as intended).
-The types `note`, `info`, `tip`, `important`, `warning`, `caution`, and `danger`
-get distinct accent colors; note and info ride the per-host accent.
+The styled types are `info`, `note`, `tip`, `todo`, `warning`, and `danger`,
+each with its own accent color and icon; `info` and `note` ride the per-host accent.
+`warn` is accepted as an alias for `warning`.
 An unknown type still renders as a plain neutral box.
 
 Callouts do not nest, by design,
diff --git a/callout.go b/callout.go
index 323deee..9d65fba 100644
--- a/callout.go
+++ b/callout.go
@@ -36,6 +36,22 @@ type calloutNode struct {
 
 var kindCallout = ast.NewNodeKind("Callout")
 
+// calloutTypeAliases folds alternate type spellings onto the canonical set that
+// style.css styles.  Kept as a table because it is cheap to grow; for now it
+// just spells warn out in full.
+var calloutTypeAliases = map[string]string{
+	"warn": "warning",
+}
+
+// canonicalCalloutType maps an author's type onto its canonical spelling,
+// leaving unknown types untouched (they render as a neutral box).
+func canonicalCalloutType(typ string) string {
+	if c, ok := calloutTypeAliases[typ]; ok {
+		return c
+	}
+	return typ
+}
+
 func (n *calloutNode) Kind() ast.NodeKind { return kindCallout }
 
 func (n *calloutNode) Dump(src []byte, level int) { ast.DumpHelper(n, src, level, nil, nil) }
@@ -89,7 +105,7 @@ func (calloutParser) Open(parent ast.Node, reader text.Reader, pc parser.Context
 		typ, title = rest[:sp], bytes.TrimSpace(rest[sp+1:])
 	}
 	reader.Advance(len(line))
-	return &calloutNode{Kind_: string(typ), Title: string(title)}, parser.HasChildren
+	return &calloutNode{Kind_: canonicalCalloutType(string(typ)), Title: string(title)}, parser.HasChildren
 }
 
 func (calloutParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
diff --git a/callout_test.go b/callout_test.go
index ec5ea0e..69f3efd 100644
--- a/callout_test.go
+++ b/callout_test.go
@@ -113,6 +113,22 @@ func TestCalloutTypeIsEscaped(t *testing.T) {
 	}
 }
 
+func TestCalloutTypeNormalized(t *testing.T) {
+	// warn folds onto the canonical warning type that style.css styles.
+	got := renderMD(t, ":::warn Heads up\nx\n:::\n")
+	if !strings.Contains(got, "callout-warning") {
+		t.Errorf("warn not normalized to warning; got:\n%s", got)
+	}
+}
+
+func TestCalloutUnknownTypePassesThrough(t *testing.T) {
+	// An unknown type is left as-is on the class, for the neutral fallback box.
+	got := renderMD(t, ":::obscure\nx\n:::\n")
+	if !strings.Contains(got, "callout callout-obscure") {
+		t.Errorf("unknown type not preserved; got:\n%s", got)
+	}
+}
+
 func TestCalloutNoNesting(t *testing.T) {
 	// An opener inside a callout is left as plain content, not a nested box.
 	got := renderMD(t, ":::note\n:::warning\ninner\n:::\n")
diff --git a/style.css b/style.css
index 76b305f..aaa65ab 100644
--- a/style.css
+++ b/style.css
@@ -109,35 +109,52 @@ article th, article td {
 }
 article th { background: var(--well); }
 
-/* Callouts: :::type admonition boxes.  Inset like other article content,
-   in a well with a bold left border, consistent with pre and blockquote.
-   The per-type accent hue rides a single --callout variable so the base box
-   below stays the one place that positions and paints the container. */
+/* Callouts: :::type admonition boxes.
+   Each type carries one accent hue in --callout, and the base box derives its
+   tinted fill and border from that hue with color-mix, so a single value drives
+   the whole look (info and note simply inherit the per-host --accent).
+   --callout-icon is the emoji shown before a title, following this repo's
+   emoji-as-icon idiom (see the heading chainlink); it rides the title, so a
+   title-less callout shows no icon. */
 article .callout {
 	--callout: var(--accent);
-	background: var(--well);
-	border: 1px solid var(--well-edge);
-	border-left: 4px solid var(--callout);
-	border-radius: .25rem;
-	padding: .6rem .8rem;
+	margin-top: 1.25rem;
+	margin-bottom: 1.25rem;
+	padding: .7rem 1rem;
+	border: 1px solid color-mix(in srgb, var(--callout) 40%, var(--well-edge));
+	border-radius: .5rem;
+	background: color-mix(in srgb, var(--callout) 9%, var(--paper));
+	box-shadow: 0 1px 3px rgba(0, 0, 0, .08);
 }
 /* The children are already inset by "article > *"; inside the box they
    should sit flush to its padding instead. */
 article .callout > * { margin-left: 0; }
 article .callout-title {
-	margin-top: 0;
+	margin: 0 0 .4rem;
 	font-weight: bold;
+	font-size: .95rem;
 	text-transform: uppercase;
 	letter-spacing: .03em;
 	color: var(--callout);
 }
+/* The icon rides the title via --callout-icon.  Only the known types below set
+   it, so an unknown type shows a clean icon-less box rather than a misleading
+   glyph; a title-less callout shows no icon at all. */
+article .callout-title::before {
+	content: var(--callout-icon, "");
+	margin-right: .45rem;
+}
 
-/* Per-type accent hues.  note/info stay in the blue --accent family;
-   the rest pick one hue each.  An unknown type matches none of these and
-   falls back to the neutral base box above, still readable. */
-article .callout-tip { --callout: #1a7f37; }
-article .callout-warning,
-article .callout-caution { --callout: #bf8700; }
-article .callout-danger { --callout: #cf222e; }
-article .callout-important { --callout: #8250df; }
+/* Per-type accent hues and icons, for the canonical type set only;
+   alternate spellings (warn, success, caution, important) are folded onto
+   these by the normalizer before they reach the class, so the styling stays
+   to one row per look.  info and note keep the per-host --accent.
+   An unknown type matches none of these and falls back to the neutral base
+   box above, still readable. */
+article .callout-info,
+article .callout-note { --callout-icon: "\2139\FE0F"; } /* information source, theme color */
+article .callout-tip { --callout: #1a7f37; --callout-icon: "\1F4A1"; } /* light bulb, green */
+article .callout-todo { --callout: #8250df; --callout-icon: "\1F4DD"; } /* memo, purple */
+article .callout-warning { --callout: #e8590c; --callout-icon: "\26A0\FE0F"; } /* warning sign, orange */
+article .callout-danger { --callout: #cf222e; --callout-icon: "\1F6D1"; } /* stop sign, red */