1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Status {
28 Confirmed,
30 Emerging,
32 Retired,
34}
35
36pub struct DataAttr {
38 pub name: &'static str,
40 pub values: &'static [&'static str],
42 pub default: &'static str,
44 pub description: &'static str,
46}
47
48pub struct ComponentEntry {
50 pub class: &'static str,
52 pub kind: &'static str,
54 pub parent: &'static str,
56 pub data_attrs: &'static [DataAttr],
58 pub example_html: &'static str,
60 pub example_markdown: &'static str,
62 pub status: Status,
64 pub since: &'static str,
66 pub description: &'static str,
68}
69
70pub const UNPREFIXED_LEGACY_CLASSES: &[&str] = &[
93 "callout",
95 "main-nav",
97 "nav-left",
98 "nav-right",
99 "nav-links",
100 "nav-icons",
101 "nav-search-btn",
102 "nav-theme-btn",
103 "nav-lang-toggle",
104 "nav-lang-current",
105 "nav-lang-link",
106 "search-icon",
107 "theme-toggle-icon",
108 "mobile-menu-button",
109 "site-name",
110 "site-logo",
111 "breadcrumb-segment",
112 "breadcrumb-label",
113 "breadcrumb-separator",
114 "date-line",
116 "date",
117 "footer-default",
119 "footer-link",
120 "container",
125 "nav-content",
126 "font-anchor",
127 "font-trigger",
128 "cover-thumb",
129 "media-item",
130 "lightbox-content",
131 "lightbox-image",
132 "lightbox-video",
133 "lightbox-iframe",
134 "lightbox-title",
135 "lightbox-article-link",
136 "lightbox-close",
137 "lightbox-next",
138 "lightbox-prev",
139 "comments-toggle",
140 "comment-list",
141 "comment-item",
142 "comment-replies",
143 "comment-reply-btn",
144];
145
146pub fn is_unprefixed_legacy(class: &str) -> bool {
151 class.starts_with("callout-") || UNPREFIXED_LEGACY_CLASSES.contains(&class)
152}
153
154pub const COMPONENTS: &[ComponentEntry] = &[
159 ComponentEntry {
160 class: "moss-cards",
161 kind: "container",
162 parent: "",
163 data_attrs: &[
164 DataAttr {
165 name: "data-layout",
166 values: &["grid", "list", "minimal"],
167 default: "grid",
168 description: "Card layout density. Grid: 2-3 cols with covers. List: single column with side covers. Minimal: text-only with year groupings.",
169 },
170 DataAttr {
171 name: "data-density",
172 values: &["default", "compact"],
173 default: "default",
174 description: "Vertical spacing density.",
175 },
176 DataAttr {
177 name: "data-list-axis",
178 values: &["date", "weight", "title"],
179 default: "title",
180 description: "Sort axis for the listing (mirrors the folder's `sort:` frontmatter). Drives `--moss-card-min` density tuning and decides whether each `.moss-card-meta` slot is filled (date axis) or omitted (weight/title axes).",
181 },
182 DataAttr {
183 name: "data-list-has-covers",
184 values: &[""],
185 default: "",
186 description: "Boolean presence flag: emitted iff any child card has a cover. Combines with `data-list-axis` to widen `--moss-card-min` for cover-led layouts. Use `[data-list-has-covers]` in CSS to target it.",
187 },
188 ],
189 example_html: r#"<div class="moss-cards-container">
190 <div class="moss-cards" data-layout="grid" data-list-axis="date" data-list-has-covers>
191 <a class="moss-card" href="...">...</a>
192 <a class="moss-card" href="...">...</a>
193 </div>
194</div>"#,
195 example_markdown: "",
196 status: Status::Confirmed,
197 since: "1",
198 description: "Auto-generated listing of child pages. The single canonical container; layout density on `data-layout` (`grid` for cover-led tiles, `list` for cover+excerpt rows, `minimal` for text-only year-grouped indexes). Wrapped in `.moss-cards-container` to scope CSS container queries.",
199 },
200 ComponentEntry {
201 class: "moss-cards-container",
202 kind: "container",
203 parent: "",
204 data_attrs: &[],
205 example_html: r#"<div class="moss-cards-container">
206 <div class="moss-cards" data-layout="grid">...</div>
207</div>"#,
208 example_markdown: "",
209 status: Status::Confirmed,
210 since: "1",
211 description: "Outer wrapper around `.moss-cards` that carries `container-type: inline-size` so the grid can use `@container` queries instead of viewport `@media` queries. Layout-agnostic — wraps any `data-layout` variant.",
212 },
213 ComponentEntry {
214 class: "moss-summary-layout",
215 kind: "container",
216 parent: "moss-cards",
217 data_attrs: &[],
218 example_html: r#"<div class="moss-cards" data-layout="list">...</div>"#,
219 example_markdown: "",
220 status: Status::Retired,
221 since: "1",
222 description: "Retired: the additional co-class on `.moss-cards[data-layout=\"list\"]` had no matching rules in the default CSS once `children_style: summary` collapsed into the list-layout block, and its lingering emission broke themes that hid the class (e.g. SoCiviC's `.moss/theme/style.css` keyed `display: none` on it, erasing folder-embed listings). Theme authors targeting summary listings should use `.moss-cards[data-layout=\"list\"]` directly.",
223 },
224 ComponentEntry {
230 class: "moss-cards-grid",
231 kind: "container",
232 parent: "",
233 data_attrs: &[],
234 example_html: r#"<div class="moss-cards-grid">
235 <a class="moss-card-grid" href="...">...</a>
236</div>"#,
237 example_markdown: "",
238 status: Status::Retired,
239 since: "0",
240 description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=grid]`.",
241 },
242 ComponentEntry {
243 class: "moss-cards-list",
244 kind: "container",
245 parent: "",
246 data_attrs: &[],
247 example_html: r#"<div class="moss-cards-list">
248 <a class="moss-card-list" href="...">...</a>
249</div>"#,
250 example_markdown: "",
251 status: Status::Retired,
252 since: "0",
253 description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=list]`.",
254 },
255 ComponentEntry {
256 class: "moss-cards-minimal-year-group",
257 kind: "container",
258 parent: "",
259 data_attrs: &[],
260 example_html: r#"<section class="moss-cards-minimal-year-group">
261 <h3>2024</h3>
262 <div class="moss-card-minimal">...</div>
263</section>"#,
264 example_markdown: "",
265 status: Status::Confirmed,
266 since: "0",
267 description: "Year-grouped section in minimal card layout (e.g. blog index). Modifier `--summary` collapses past years.",
268 },
269 ComponentEntry {
270 class: "moss-cards-minimal-year-group--summary",
271 kind: "container",
272 parent: "moss-cards-minimal-year-group",
273 data_attrs: &[],
274 example_html: r#"<section class="moss-cards-minimal-year-group moss-cards-minimal-year-group--summary">...</section>"#,
275 example_markdown: "",
276 status: Status::Confirmed,
277 since: "0",
278 description: "BEM modifier on `.moss-cards-minimal-year-group`. Applied to year groups that should render in collapsed summary form (e.g. past years on a blog index).",
279 },
280 ComponentEntry {
281 class: "moss-card",
282 kind: "instance",
283 parent: "moss-cards",
284 data_attrs: &[
285 DataAttr {
286 name: "data-linkblog",
287 values: &[],
288 default: "",
289 description: "Presence flag: emitted IFF the card's source page has an `external_url:` frontmatter (linkblog pattern). When set, the element is a `<div>` rather than `<a>` so the kicker can host a nested `<a>★</a>` archive link; title, cover, and description carry their own inner anchors to the canonical URL. Absent on ordinary cards (single whole-card `<a>`).",
290 },
291 DataAttr {
292 name: "data-cover-color",
293 values: &[],
294 default: "",
295 description: "Presence flag: emitted IFF the cover-colour ladder produced a colour, which arrives alongside it as `--moss-cover-color` in the element's `style`. Absent for a card with no cover, and for an image cover whose file moss could not read.",
296 },
297 ],
298 example_html: r#"<a class="moss-card" href="...">...</a>"#,
299 example_markdown: "",
300 status: Status::Confirmed,
301 since: "1",
302 description: "v1 collapsed shape — single canonical instance class inside `.moss-cards`. Layout-specific styling targets `.moss-cards[data-layout=X] .moss-card`. Tag is `<a>` for ordinary cards and `<div>` for linkblog cards (`[data-linkblog]`).",
303 },
304 ComponentEntry {
305 class: "moss-card-cover",
306 kind: "instance",
307 parent: "moss-card",
308 data_attrs: &[],
309 example_html: r#"<div class="moss-card-cover"><img src="..." /></div>"#,
310 example_markdown: "",
311 status: Status::Confirmed,
312 since: "1",
313 description: "Cover media slot inside `.moss-card`. Gets `.moss-card-no-cover` modifier when no image is present.",
314 },
315 ComponentEntry {
316 class: "moss-card-no-cover",
317 kind: "instance",
318 parent: "moss-card",
319 data_attrs: &[],
320 example_html: r#"<div class="moss-card-cover moss-card-no-cover"></div>"#,
321 example_markdown: "",
322 status: Status::Confirmed,
323 since: "1",
324 description: "Modifier applied to `.moss-card-cover` when no cover media is available.",
325 },
326 ComponentEntry {
327 class: "moss-card-content",
328 kind: "instance",
329 parent: "moss-card",
330 data_attrs: &[],
331 example_html: r#"<div class="moss-card-content">...</div>"#,
332 example_markdown: "",
333 status: Status::Confirmed,
334 since: "1",
335 description: "Text content slot inside a grid-layout `.moss-card` (kicker + title + meta).",
336 },
337 ComponentEntry {
338 class: "moss-card-row",
339 kind: "instance",
340 parent: "moss-card",
341 data_attrs: &[],
342 example_html: r#"<div class="moss-card-row">...</div>"#,
343 example_markdown: "",
344 status: Status::Confirmed,
345 since: "1",
346 description: "Row wrapper inside a list-layout `.moss-card` holding body + cover side-by-side.",
347 },
348 ComponentEntry {
349 class: "moss-card-body",
350 kind: "instance",
351 parent: "moss-card",
352 data_attrs: &[],
353 example_html: r#"<div class="moss-card-body">...</div>"#,
354 example_markdown: "",
355 status: Status::Confirmed,
356 since: "1",
357 description: "Text body slot of a list-layout `.moss-card`.",
358 },
359 ComponentEntry {
360 class: "moss-card-head",
361 kind: "instance",
362 parent: "moss-card",
363 data_attrs: &[],
364 example_html: r#"<div class="moss-card-head">...</div>"#,
365 example_markdown: "",
366 status: Status::Confirmed,
367 since: "1",
368 description: "Header row of a `.moss-card-body` (title + kicker + meta).",
369 },
370 ComponentEntry {
371 class: "moss-card-title",
372 kind: "instance",
373 parent: "moss-card",
374 data_attrs: &[],
375 example_html: r#"<h3 class="moss-card-title">Page title</h3>"#,
376 example_markdown: "",
377 status: Status::Confirmed,
378 since: "1",
379 description: "Title inside `.moss-card`.",
380 },
381 ComponentEntry {
382 class: "moss-card-meta",
383 kind: "instance",
384 parent: "moss-card",
385 data_attrs: &[],
386 example_html: r#"<div class="moss-card-meta">2024-01-15</div>"#,
387 example_markdown: "",
388 status: Status::Confirmed,
389 since: "1",
390 description: "Type-aware metadata slot (date for articles, count for folders, domain for links). Renders ABOVE the title in horizontal mode — filling the kicker position when the explicit `kicker` slot is unset, per `docs/reference/design/preview-cards.md:22-30`. To the right of the title in vertical CJK mode (the horizontal kicker position transposed). Meta IS the visual kicker, with the same uppercase overline treatment.",
391 },
392 ComponentEntry {
393 class: "moss-card-kicker",
394 kind: "instance",
395 parent: "moss-card",
396 data_attrs: &[],
397 example_html: r#"<span class="moss-card-kicker">Category</span>"#,
398 example_markdown: "",
399 status: Status::Confirmed,
400 since: "1",
401 description: "Eyebrow / overline above the title inside `.moss-card`.",
402 },
403 ComponentEntry {
404 class: "moss-card-permalink",
405 kind: "instance",
406 parent: "moss-card-kicker",
407 data_attrs: &[],
408 example_html: r#"<a class="moss-card-permalink" href="/posts/foo/" title="Permalink to 'Title'">★</a>"#,
409 example_markdown: "",
410 status: Status::Emerging,
411 since: "1",
412 description: "Author's-archive link mark (★, U+2605) emitted INSIDE `.moss-card-kicker` for linkblog cards (those whose child page has `external_url:`). The card title links to the external canonical (publisher); the `★` links to the local archival copy at the page's slug. Reads as part of the kicker line — \"Publisher · Year ★\". Semantically distinct from Daring-Fireball's linkblog ★ (which marks discussion permalink alongside commentary) — here the local copy is the same content preserved for resilience and stable bylines, not added commentary. Putting `<a>★</a>` inside the kicker is valid because linkblog cards emit `<div class=\"moss-card\" data-linkblog>` (not `<a>`) as the outer element — see the `data-linkblog` attribute described on `.moss-card`.",
413 },
414 ComponentEntry {
415 class: "moss-card-title-link",
416 kind: "instance",
417 parent: "moss-card-head",
418 data_attrs: &[],
419 example_html: r#"<a class="moss-card-title-link" href="https://outlet.example/article"><h3 class="moss-card-title">Article Title</h3></a>"#,
420 example_markdown: "",
421 status: Status::Emerging,
422 since: "1",
423 description: "Anchor wrapping the `.moss-card-title` `<h3>` on linkblog cards. Ordinary cards have the whole-card `<a class=\"moss-card\">` as the link target — but linkblog cards switch the outer to `<div>` so the kicker can host a nested `★` anchor, which means the title needs its own anchor to stay clickable. Same canonical-URL target as the other inner anchors (`moss-card-cover-link`, `moss-card-description-link`).",
424 },
425 ComponentEntry {
426 class: "moss-card-cover-link",
427 kind: "instance",
428 parent: "moss-card-row",
429 data_attrs: &[],
430 example_html: r#"<a class="moss-card-cover-link" href="https://outlet.example/article"><div class="moss-card-cover">...</div></a>"#,
431 example_markdown: "",
432 status: Status::Emerging,
433 since: "1",
434 description: "Anchor wrapping the `.moss-card-cover` on linkblog cards — same role as `.moss-card-title-link` but for the cover image / media. Targets the canonical (external) URL.",
435 },
436 ComponentEntry {
437 class: "moss-card-description-link",
438 kind: "instance",
439 parent: "moss-card-body",
440 data_attrs: &[],
441 example_html: r#"<a class="moss-card-description-link" href="https://outlet.example/article"><p class="moss-card-description">…</p></a>"#,
442 example_markdown: "",
443 status: Status::Emerging,
444 since: "1",
445 description: "Anchor wrapping the `.moss-card-description` on linkblog cards — same role as `.moss-card-title-link` but for the description excerpt. Targets the canonical (external) URL.",
446 },
447 ComponentEntry {
448 class: "moss-card-description",
449 kind: "instance",
450 parent: "moss-card",
451 data_attrs: &[],
452 example_html: r#"<p class="moss-card-description">Excerpt...</p>"#,
453 example_markdown: "",
454 status: Status::Confirmed,
455 since: "1",
456 description: "Excerpt / description paragraph inside a `.moss-card` — below the title in both grid- and list-layout cards.",
457 },
458 ComponentEntry {
459 class: "moss-card-count",
460 kind: "instance",
461 parent: "moss-card",
462 data_attrs: &[],
463 example_html: r#"<div class="moss-card-count">4 articles</div>"#,
464 example_markdown: "",
465 status: Status::Confirmed,
466 since: "1",
467 description: "Tertiary subtitle line showing `N articles` for a folder card. Renders only on non-date listings when the folder card has no `description` to display.",
468 },
469 ComponentEntry {
470 class: "moss-embed-more",
471 kind: "instance",
472 parent: "moss-cards-container",
473 data_attrs: &[],
474 example_html: r#"<p class="moss-embed-more"><a href="/news/">More →</a></p>"#,
475 example_markdown: "",
476 status: Status::Confirmed,
477 since: "1",
478 description: "Trailing \"More →\" link on a truncated children listing (emitted when `children_limit` caps the embed); links to the folder's full index. Rendered as a sibling immediately after `.moss-cards-container`, so it sits outside the listing's flex `gap` and binds to the list via its own `margin-top` (see docs/reference/design/spacing.md).",
479 },
480 ComponentEntry {
481 class: "moss-card-grid",
482 kind: "instance",
483 parent: "moss-cards-grid",
484 data_attrs: &[],
485 example_html: r#"<a class="moss-card-grid" href="...">...</a>"#,
486 example_markdown: "",
487 status: Status::Retired,
488 since: "0",
489 description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=grid]`).",
490 },
491 ComponentEntry {
492 class: "moss-card-grid-cover",
493 kind: "instance",
494 parent: "moss-card-grid",
495 data_attrs: &[],
496 example_html: r#"<div class="moss-card-grid-cover"><img src="..." /></div>"#,
497 example_markdown: "",
498 status: Status::Retired,
499 since: "0",
500 description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
501 },
502 ComponentEntry {
503 class: "moss-card-grid-no-cover",
504 kind: "instance",
505 parent: "moss-card-grid",
506 data_attrs: &[],
507 example_html: r#"<div class="moss-card-grid-cover moss-card-grid-no-cover"></div>"#,
508 example_markdown: "",
509 status: Status::Retired,
510 since: "0",
511 description: "Retired in Phase 1c — collapsed into `.moss-card-no-cover`.",
512 },
513 ComponentEntry {
514 class: "moss-card-grid-content",
515 kind: "instance",
516 parent: "moss-card-grid",
517 data_attrs: &[],
518 example_html: r#"<div class="moss-card-grid-content">...</div>"#,
519 example_markdown: "",
520 status: Status::Retired,
521 since: "0",
522 description: "Retired in Phase 1c — collapsed into `.moss-card-content`.",
523 },
524 ComponentEntry {
525 class: "moss-card-grid-kicker",
526 kind: "instance",
527 parent: "moss-card-grid",
528 data_attrs: &[],
529 example_html: r#"<span class="moss-card-grid-kicker">Category</span>"#,
530 example_markdown: "",
531 status: Status::Retired,
532 since: "0",
533 description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
534 },
535 ComponentEntry {
536 class: "moss-card-grid-title",
537 kind: "instance",
538 parent: "moss-card-grid",
539 data_attrs: &[],
540 example_html: r#"<h3 class="moss-card-grid-title">Page title</h3>"#,
541 example_markdown: "",
542 status: Status::Retired,
543 since: "0",
544 description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
545 },
546 ComponentEntry {
547 class: "moss-card-grid-meta",
548 kind: "instance",
549 parent: "moss-card-grid",
550 data_attrs: &[],
551 example_html: r#"<div class="moss-card-grid-meta">2024-01-15</div>"#,
552 example_markdown: "",
553 status: Status::Retired,
554 since: "0",
555 description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
556 },
557 ComponentEntry {
558 class: "moss-card-list",
559 kind: "instance",
560 parent: "moss-cards-list",
561 data_attrs: &[],
562 example_html: r#"<a class="moss-card-list" href="...">...</a>"#,
563 example_markdown: "",
564 status: Status::Retired,
565 since: "0",
566 description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=list]`).",
567 },
568 ComponentEntry {
569 class: "moss-card-list-row",
570 kind: "instance",
571 parent: "moss-card-list",
572 data_attrs: &[],
573 example_html: r#"<div class="moss-card-list-row">...</div>"#,
574 example_markdown: "",
575 status: Status::Retired,
576 since: "0",
577 description: "Retired in Phase 1c — collapsed into `.moss-card-row`.",
578 },
579 ComponentEntry {
580 class: "moss-card-list-cover",
581 kind: "instance",
582 parent: "moss-card-list",
583 data_attrs: &[],
584 example_html: r#"<div class="moss-card-list-cover"><img src="..." /></div>"#,
585 example_markdown: "",
586 status: Status::Retired,
587 since: "0",
588 description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
589 },
590 ComponentEntry {
591 class: "moss-card-list-body",
592 kind: "instance",
593 parent: "moss-card-list",
594 data_attrs: &[],
595 example_html: r#"<div class="moss-card-list-body">...</div>"#,
596 example_markdown: "",
597 status: Status::Retired,
598 since: "0",
599 description: "Retired in Phase 1c — collapsed into `.moss-card-body`.",
600 },
601 ComponentEntry {
602 class: "moss-card-list-head",
603 kind: "instance",
604 parent: "moss-card-list",
605 data_attrs: &[],
606 example_html: r#"<div class="moss-card-list-head">...</div>"#,
607 example_markdown: "",
608 status: Status::Retired,
609 since: "0",
610 description: "Retired in Phase 1c — collapsed into `.moss-card-head`.",
611 },
612 ComponentEntry {
613 class: "moss-card-list-kicker",
614 kind: "instance",
615 parent: "moss-card-list",
616 data_attrs: &[],
617 example_html: r#"<span class="moss-card-list-kicker">Category</span>"#,
618 example_markdown: "",
619 status: Status::Retired,
620 since: "0",
621 description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
622 },
623 ComponentEntry {
624 class: "moss-card-list-title",
625 kind: "instance",
626 parent: "moss-card-list",
627 data_attrs: &[],
628 example_html: r#"<h3 class="moss-card-list-title">Page title</h3>"#,
629 example_markdown: "",
630 status: Status::Retired,
631 since: "0",
632 description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
633 },
634 ComponentEntry {
635 class: "moss-card-list-meta",
636 kind: "instance",
637 parent: "moss-card-list",
638 data_attrs: &[],
639 example_html: r#"<div class="moss-card-list-meta">2024-01-15</div>"#,
640 example_markdown: "",
641 status: Status::Retired,
642 since: "0",
643 description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
644 },
645 ComponentEntry {
646 class: "moss-card-list-description",
647 kind: "instance",
648 parent: "moss-card-list",
649 data_attrs: &[],
650 example_html: r#"<p class="moss-card-list-description">Excerpt...</p>"#,
651 example_markdown: "",
652 status: Status::Retired,
653 since: "0",
654 description: "Retired in Phase 1c — collapsed into `.moss-card-description`.",
655 },
656 ComponentEntry {
657 class: "moss-card-minimal",
658 kind: "instance",
659 parent: "moss-cards-minimal-year-group",
660 data_attrs: &[],
661 example_html: r#"<div class="moss-card-minimal">
662 <a class="moss-prefix-link" href="...">...</a>
663</div>"#,
664 example_markdown: "",
665 status: Status::Retired,
666 since: "0",
667 description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=minimal]`).",
668 },
669 ComponentEntry {
670 class: "moss-folder-item",
671 kind: "instance",
672 parent: "moss-cards-minimal-year-group",
673 data_attrs: &[],
674 example_html: r#"<div class="moss-card-minimal moss-folder-item">
675 <a class="moss-prefix-link moss-folder-link" href="...">...</a>
676 <p class="moss-folder-description">...</p>
677</div>"#,
678 example_markdown: "",
679 status: Status::Confirmed,
680 since: "0",
681 description: "Modifier on `.moss-card-minimal` for folder-type entries in minimal listings.",
682 },
683 ComponentEntry {
684 class: "moss-folder-title",
685 kind: "instance",
686 parent: "moss-folder-item",
687 data_attrs: &[],
688 example_html: r#"<span class="moss-folder-title">Folder name</span>"#,
689 example_markdown: "",
690 status: Status::Confirmed,
691 since: "0",
692 description: "Title text of a folder entry in minimal listings.",
693 },
694 ComponentEntry {
695 class: "moss-folder-description",
696 kind: "instance",
697 parent: "moss-folder-item",
698 data_attrs: &[],
699 example_html: r#"<p class="moss-folder-description">Description...</p>"#,
700 example_markdown: "",
701 status: Status::Confirmed,
702 since: "0",
703 description: "Description paragraph of a folder entry in minimal listings.",
704 },
705 ComponentEntry {
706 class: "moss-folder-link",
707 kind: "instance",
708 parent: "moss-folder-item",
709 data_attrs: &[],
710 example_html: r#"<a class="moss-prefix-link moss-folder-link" href="...">...</a>"#,
711 example_markdown: "",
712 status: Status::Confirmed,
713 since: "0",
714 description: "Modifier on `.moss-prefix-link` for folder-type links in minimal listings.",
715 },
716 ComponentEntry {
720 class: "moss-prefix-link",
721 kind: "instance",
722 parent: "moss-card-minimal",
723 data_attrs: &[],
724 example_html: r#"<a class="moss-prefix-link" href="...">
725 <span class="moss-prefix-link-prefix">2024-01-15</span>
726 <span class="moss-prefix-link-title">Page title</span>
727</a>"#,
728 example_markdown: "",
729 status: Status::Emerging,
730 since: "0",
731 description: "Link with a prefix span (date or icon) and a title span. Used inside minimal cards.",
732 },
733 ComponentEntry {
734 class: "moss-prefix-link-prefix",
735 kind: "instance",
736 parent: "moss-prefix-link",
737 data_attrs: &[],
738 example_html: r#"<span class="moss-prefix-link-prefix">2024-01-15</span>"#,
739 example_markdown: "",
740 status: Status::Emerging,
741 since: "0",
742 description: "Prefix slot of a prefix-link (typically a date).",
743 },
744 ComponentEntry {
745 class: "moss-prefix-link-title",
746 kind: "instance",
747 parent: "moss-prefix-link",
748 data_attrs: &[],
749 example_html: r#"<span class="moss-prefix-link-title">Page title</span>"#,
750 example_markdown: "",
751 status: Status::Emerging,
752 since: "0",
753 description: "Title slot of a prefix-link.",
754 },
755 ComponentEntry {
756 class: "moss-prefix-link-suffix",
757 kind: "instance",
758 parent: "moss-prefix-link",
759 data_attrs: &[],
760 example_html: r#"<span class="moss-prefix-link-suffix">→</span>"#,
761 example_markdown: "",
762 status: Status::Emerging,
763 since: "0",
764 description: "Optional trailing slot of a prefix-link.",
765 },
766 ComponentEntry {
772 class: "moss-callout",
773 kind: "standalone",
774 parent: "",
775 data_attrs: &[],
776 example_html: r#"<div class="moss-callout callout" data-type="note">
777 <div class="callout-title">Note</div>
778 <div class="callout-content">Body...</div>
779</div>"#,
780 example_markdown: "> [!note]\n> Body...",
781 status: Status::Confirmed,
782 since: "0",
783 description: "Obsidian-style callout. The Obsidian-compat `.callout` class is co-emitted; type lives on `data-type` (v1).",
784 },
785 ComponentEntry {
786 class: "callout",
787 kind: "standalone",
788 parent: "",
789 data_attrs: &[
790 DataAttr {
791 name: "data-type",
792 values: &["note", "info", "tip", "warning", "pending"],
793 default: "note",
794 description: "v1 callout type. Theme authors target `.callout[data-type=...]` to style by variant.",
795 },
796 ],
797 example_html: r#"<div class="moss-callout callout" data-type="note">...</div>"#,
798 example_markdown: "",
799 status: Status::Confirmed,
800 since: "0",
801 description: "Obsidian-compat class co-emitted on every callout for theme parity. Type lives on `data-type` (v1).",
802 },
803 ComponentEntry {
804 class: "callout-title",
805 kind: "instance",
806 parent: "moss-callout",
807 data_attrs: &[],
808 example_html: r#"<div class="callout-title">Note</div>"#,
809 example_markdown: "",
810 status: Status::Confirmed,
811 since: "0",
812 description: "Title row of a callout.",
813 },
814 ComponentEntry {
815 class: "callout-content",
816 kind: "instance",
817 parent: "moss-callout",
818 data_attrs: &[],
819 example_html: r#"<div class="callout-content">Body...</div>"#,
820 example_markdown: "",
821 status: Status::Confirmed,
822 since: "0",
823 description: "Body container of a callout.",
824 },
825 ComponentEntry {
826 class: "callout-note",
827 kind: "instance",
828 parent: "moss-callout",
829 data_attrs: &[],
830 example_html: r#"<div class="moss-callout callout callout-note">...</div>"#,
831 example_markdown: "> [!note]\n> Body",
832 status: Status::Retired,
833 since: "0",
834 description: "Retired in Phase 1c — type lives on `.callout[data-type=note]`.",
835 },
836 ComponentEntry {
837 class: "callout-info",
838 kind: "instance",
839 parent: "moss-callout",
840 data_attrs: &[],
841 example_html: r#"<div class="moss-callout callout callout-info">...</div>"#,
842 example_markdown: "> [!info]\n> Body",
843 status: Status::Retired,
844 since: "0",
845 description: "Retired in Phase 1c — type lives on `.callout[data-type=info]`.",
846 },
847 ComponentEntry {
848 class: "callout-tip",
849 kind: "instance",
850 parent: "moss-callout",
851 data_attrs: &[],
852 example_html: r#"<div class="moss-callout callout callout-tip">...</div>"#,
853 example_markdown: "> [!tip]\n> Body",
854 status: Status::Retired,
855 since: "0",
856 description: "Retired in Phase 1c — type lives on `.callout[data-type=tip]`.",
857 },
858 ComponentEntry {
859 class: "callout-warning",
860 kind: "instance",
861 parent: "moss-callout",
862 data_attrs: &[],
863 example_html: r#"<div class="moss-callout callout callout-warning">...</div>"#,
864 example_markdown: "> [!warning]\n> Body",
865 status: Status::Retired,
866 since: "0",
867 description: "Retired in Phase 1c — type lives on `.callout[data-type=warning]`.",
868 },
869 ComponentEntry {
870 class: "callout-pending",
871 kind: "instance",
872 parent: "moss-callout",
873 data_attrs: &[],
874 example_html: r#"<div class="moss-callout callout callout-pending">...</div>"#,
875 example_markdown: "> [!pending]\n> Body",
876 status: Status::Retired,
877 since: "0",
878 description: "Retired in Phase 1c — type lives on `.callout[data-type=pending]`.",
879 },
880 ComponentEntry {
885 class: "moss-embed",
886 kind: "standalone",
887 parent: "",
888 data_attrs: &[
889 DataAttr {
890 name: "data-type",
891 values: &["audio", "video", "pdf", "notebook", "table", "iframe", "3d"],
892 default: "",
893 description: "v1 embed kind. Set on the embed element. Theme authors target `.moss-embed[data-type=...]`.",
894 },
895 DataAttr {
896 name: "data-loop",
897 values: &[],
898 default: "",
899 description: "Ambient background video: autoplay + muted + loop + playsinline, controls off. Authored as `![[clip.mp4|loop]]`. Boolean presence flag (value is empty). JS reads it to apply the reduced-motion guard and mount the pause/play toggle.",
900 },
901 DataAttr {
902 name: "data-width",
903 values: &["body", "wide", "page", "screen"],
904 default: "body",
905 description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
906 },
907 DataAttr {
908 name: "data-provider",
909 values: &["youtube", "vimeo", "codepen"],
910 default: "",
911 description: "Identifies the embed provider for external URL embeds. Absent for generic iframes and local HTML embeds.",
912 },
913 ],
914 example_html: r#"<video class="moss-embed moss-embed-video" data-type="video" data-loop src="clip.mp4" autoplay muted loop playsinline preload="metadata"></video>"#,
915 example_markdown: "![[clip.mp4|loop]]",
916 status: Status::Confirmed,
917 since: "0",
918 description: "Base class on every typed embed. Kind on `data-type` (v1). Ambient video: add `data-loop` via `![[clip.mp4|loop]]`. `.moss-embed-audio` / `-video` / `-pdf` / `-notebook` / `-table` / `-iframe` / `-3d` retired in Phase 1c.",
919 },
920 ComponentEntry {
921 class: "moss-embed-audio",
922 kind: "instance",
923 parent: "moss-embed",
924 data_attrs: &[],
925 example_html: r#"<div class="moss-embed moss-embed-audio"><audio controls src="..."></audio></div>"#,
926 example_markdown: "![[track.mp3]]",
927 status: Status::Retired,
928 since: "0",
929 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=audio]`.",
930 },
931 ComponentEntry {
932 class: "moss-embed-video",
933 kind: "instance",
934 parent: "moss-embed",
935 data_attrs: &[],
936 example_html: r#"<div class="moss-embed moss-embed-video"><video controls src="..."></video></div>"#,
937 example_markdown: "![[clip.mp4]]",
938 status: Status::Retired,
939 since: "0",
940 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=video]`.",
941 },
942 ComponentEntry {
943 class: "moss-embed-pdf",
944 kind: "instance",
945 parent: "moss-embed",
946 data_attrs: &[],
947 example_html: r#"<div class="moss-embed moss-embed-pdf"><iframe src="..."></iframe></div>"#,
948 example_markdown: "![[paper.pdf]]",
949 status: Status::Retired,
950 since: "0",
951 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=pdf]`.",
952 },
953 ComponentEntry {
954 class: "moss-embed-iframe",
955 kind: "instance",
956 parent: "moss-embed",
957 data_attrs: &[],
958 example_html: r#"<div class="moss-embed moss-embed-iframe"><iframe src="..."></iframe></div>"#,
959 example_markdown: "![[page.html]]",
960 status: Status::Retired,
961 since: "0",
962 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=iframe]`.",
963 },
964 ComponentEntry {
965 class: "moss-embed-notebook",
966 kind: "instance",
967 parent: "moss-embed",
968 data_attrs: &[],
969 example_html: r#"<div class="moss-embed moss-embed-notebook">...</div>"#,
970 example_markdown: "![[analysis.ipynb]]",
971 status: Status::Retired,
972 since: "0",
973 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=notebook]`.",
974 },
975 ComponentEntry {
976 class: "moss-embed-ipynb",
977 kind: "instance",
978 parent: "moss-embed",
979 data_attrs: &[],
980 example_html: r#"<div class="moss-embed moss-embed-ipynb">...</div>"#,
981 example_markdown: "",
982 status: Status::Emerging,
983 since: "0",
984 description: "Alias of `.moss-embed-notebook`; consolidation pending.",
985 },
986 ComponentEntry {
987 class: "moss-embed-table",
988 kind: "instance",
989 parent: "moss-embed",
990 data_attrs: &[],
991 example_html: r#"<div class="moss-embed moss-embed-table"><table>...</table></div>"#,
992 example_markdown: "![[data.csv]]",
993 status: Status::Retired,
994 since: "0",
995 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=table]`.",
996 },
997 ComponentEntry {
998 class: "moss-embed-3d",
999 kind: "instance",
1000 parent: "moss-embed",
1001 data_attrs: &[],
1002 example_html: r#"<div class="moss-embed moss-embed-3d">...</div>"#,
1003 example_markdown: "![[model.glb]]",
1004 status: Status::Retired,
1005 since: "0",
1006 description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=3d]`.",
1007 },
1008 ComponentEntry {
1009 class: "moss-embed-error",
1010 kind: "instance",
1011 parent: "moss-embed",
1012 data_attrs: &[],
1013 example_html: r#"<div class="moss-embed moss-embed-error">File not found: ...</div>"#,
1014 example_markdown: "",
1015 status: Status::Confirmed,
1016 since: "0",
1017 description: "Error state for embeds whose target cannot be resolved.",
1018 },
1019 ComponentEntry {
1020 class: "moss-embed-missing",
1021 kind: "instance",
1022 parent: "moss-embed",
1023 data_attrs: &[],
1024 example_html: r#"<div class="moss-embed-missing">Folder not found: journal</div>"#,
1025 example_markdown: "",
1026 status: Status::Confirmed,
1027 since: "1",
1028 description: "Fallback rendered when a folder-list embed (`![[journal/]]`) targets a folder that does not exist or cannot be resolved. Distinct from `.moss-embed-error` (file/wikilink resolution failure) — this one is specifically the folder-listing path.",
1029 },
1030 ComponentEntry {
1034 class: "moss-hero",
1035 kind: "standalone",
1036 parent: "",
1037 data_attrs: &[
1038 DataAttr {
1039 name: "data-width",
1040 values: &["body", "wide", "page", "screen"],
1041 default: "body",
1042 description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9. Emitted from the authoring shortcode (e.g. `:::hero {full}` -> `data-width=\"screen\"`); on article children, site.css sizes the band via the content-width escape (ADR-021 Corollary 2). The hero itself escapes by DOM position (outside `<main>`), not by these rules.",
1043 },
1044 DataAttr {
1045 name: "data-slides",
1046 values: &["2", "3", "4", "5", "6"],
1047 default: "",
1048 description: "Slide count of a multi-image hero (consecutive leading media lines). Present only when > 1; drives the ambient CSS crossfade — one slide visible at a time, no controls. Absent = single-image hero, today's exact markup.",
1049 },
1050 DataAttr {
1051 name: "data-hero-tone",
1052 values: &["light"],
1053 default: "",
1054 description: "Marks a hero whose image is pale enough (scan-cached dominant colour above 0.4 relative luminance) that white overlay text cannot clear 4.5:1 over it. site.css responds by removing the legibility scrim and setting the overlay text DARK — a pale image needs no scrim to carry dark type, and darkening it hard enough to carry white type (this rule used to reach 0.78 black) greys out the picture the author chose. Applies to the two overlaid layouts; mobile-stacked text sits on `--moss-cover-color` and stays white. Emitted only when the hero also carries overlay text. Absent = mid-tone or dark image, no overlay, or an unparseable colour — all keep white type over the default ramp.",
1055 },
1056 DataAttr {
1057 name: "data-mobile",
1058 values: &["overlay"],
1059 default: "",
1060 description: "Below **48rem** (moss's mobile threshold), `overlay` keeps the title on top of the image instead of stacking it underneath. Emitted **only** for `:::hero {mobile=overlay}` — an author must ask for it; a hero with overlay text does not get it by default. The selector to fight if you want the other behaviour is `.moss-hero[data-mobile=\"overlay\"]`.",
1061 },
1062 DataAttr {
1063 name: "data-captioned",
1064 values: &[""],
1065 default: "",
1066 description: "Present when the hero carries `caption=\"…\"`. Such a hero is a photograph on display rather than a backdrop for overlay text, and a caption that names a subject is a promise the subject is in frame — so site.css shows the whole image instead of the default crop-to-fill: the box takes the picture's own shape, centred, bounded by `--moss-hero-max-height` rather than filling the frame. Put the crop back on the image itself (`image=cover.jpg|cover top`), which lands as an inline style and wins.",
1067 },
1068 ],
1069 example_html: r#"<section class="moss-hero" data-width="page">
1070 <div class="moss-hero-content">...</div>
1071</section>"#,
1072 example_markdown: ":::hero {image=cover.jpg}\n:::\n\n:::hero {image=cover.jpg full mobile=overlay}\n# Title over the image\n:::\n",
1073 status: Status::Confirmed,
1074 since: "0",
1075 description: "Hero banner section at the top of a page (cover image + title). v1 adds `data-width` for author-controlled sizing.",
1076 },
1077 ComponentEntry {
1078 class: "moss-hero-content",
1079 kind: "instance",
1080 parent: "moss-hero",
1081 data_attrs: &[],
1082 example_html: r#"<div class="moss-hero-content">...</div>"#,
1083 example_markdown: "",
1084 status: Status::Confirmed,
1085 since: "0",
1086 description: "Text content slot inside `.moss-hero` — text laid ON the image. For text ABOUT the image, see `.moss-hero-caption`.",
1087 },
1088 ComponentEntry {
1089 class: "moss-hero-caption",
1090 kind: "standalone",
1091 parent: "",
1092 data_attrs: &[],
1093 example_html: r#"<p class="moss-hero-caption">封面:基輔米迦勒修道院門口的陣亡將士紀念牆(拍攝:糜緒洋)</p>"#,
1094 example_markdown: ":::hero {image=cover.jpg caption=\"Cover: the memorial wall (photo: A. Photographer)\"}\n:::\n",
1095 status: Status::Confirmed,
1096 since: "0",
1097 description: "Caption or credit for a hero image, from `:::hero {caption=\"…\"}`. A SIBLING of `.moss-hero`, immediately after it — not a child: the section is a fixed-height cropping frame, and a photographer's credit has to survive as text below the picture rather than be printed across it. Rendered as inline markdown, so a credit can be a link, exactly like a `byline:` / `colophon:` row.",
1098 },
1099 ComponentEntry {
1100 class: "moss-hero-slides",
1101 kind: "instance",
1102 parent: "moss-hero",
1103 data_attrs: &[],
1104 example_html: r#"<div class="moss-hero-slides"><div class="moss-hero-slide"><img src="portrait-1.jpg" alt="" /></div></div>"#,
1105 example_markdown: "",
1106 status: Status::Confirmed,
1107 since: "0",
1108 description: "Wrapper holding the `.moss-hero-slide` images of a multi-image hero; the CSS ambient crossfade cycles one slide visible at a time.",
1109 },
1110 ComponentEntry {
1111 class: "moss-hero-slide",
1112 kind: "instance",
1113 parent: "moss-hero",
1114 data_attrs: &[],
1115 example_html: r#"<div class="moss-hero-slide"><img src="portrait-1.jpg" alt="" /></div>"#,
1116 example_markdown: ":::hero
1117![[portrait-1.jpg]]
1118![[portrait-2.jpg]]
1119# Title
1120:::
1121",
1122 status: Status::Confirmed,
1123 since: "0",
1124 description: "One background slide of a multi-image hero. Emitted only when the hero has 2+ images; slides crossfade ambiently via site.css keyed on the section's data-slides. First slide is the reduced-motion static fallback.",
1125 },
1126 ComponentEntry {
1127 class: "moss-image",
1128 kind: "standalone",
1129 parent: "",
1130 data_attrs: &[
1131 DataAttr {
1132 name: "data-aspect",
1133 values: &["portrait", "square", "auto"],
1134 default: "auto",
1135 description: "v1 image aspect-ratio hint. Theme authors target `.moss-image[data-aspect=...]`. Emitter wiring lands in a follow-up.",
1136 },
1137 DataAttr {
1138 name: "data-width",
1139 values: &["body", "wide", "page", "screen"],
1140 default: "body",
1141 description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1142 },
1143 ],
1144 example_html: r#"<figure class="moss-image" style="width:55%"><img src="..." alt="..." /></figure>"#,
1145 example_markdown: "",
1146 status: Status::Confirmed,
1147 since: "0",
1148 description: "Wrapper around an inline `<img>` for sizing and figure semantics. `data-width` carries a named width token (body|wide|page|screen); a content-relative width is instead emitted as inline `style=\"width:NN%\"` (set by the editor drag-resize), which also forces the inner image to fill that percent box. Images narrower than the content column center horizontally.",
1149 },
1150 ComponentEntry {
1151 class: "moss-align-left",
1152 kind: "standalone",
1153 parent: "",
1154 data_attrs: &[],
1155 example_html: r#"<img src="..." alt="..." class="moss-align-left" />"#,
1156 example_markdown: "![[photo.jpg|align-left]]",
1157 status: Status::Confirmed,
1158 since: "0",
1159 description: "Floats an image to the left of body text (editorial runaround). Defaults max-width to 50% on desktop, collapses to full-width below 48rem. CSS `:has()` escalates the float to a wrapping `<figure class=\"moss-image\">` or `<picture>` when present. Mirrors WordPress's `alignleft` convention.",
1160 },
1161 ComponentEntry {
1162 class: "moss-align-right",
1163 kind: "standalone",
1164 parent: "",
1165 data_attrs: &[],
1166 example_html: r#"<img src="..." alt="..." class="moss-align-right" />"#,
1167 example_markdown: "![[photo.jpg|align-right]]",
1168 status: Status::Confirmed,
1169 since: "0",
1170 description: "Floats an image to the right of body text (editorial runaround). Symmetric counterpart to `.moss-align-left`. Mirrors WordPress's `alignright` convention.",
1171 },
1172 ComponentEntry {
1173 class: "moss-article-title",
1174 kind: "instance",
1175 parent: "",
1176 data_attrs: &[],
1177 example_html: r#"<h1 class="moss-article-title">Title</h1>"#,
1178 example_markdown: "",
1179 status: Status::Emerging,
1180 since: "0",
1181 description: "Article-page H1 title emitted from frontmatter.",
1182 },
1183 ComponentEntry {
1184 class: "moss-heading-anchor",
1185 kind: "instance",
1186 parent: "",
1187 data_attrs: &[],
1188 example_html: r##"<h2 id="setup">Setup<a class="moss-heading-anchor" href="#setup" aria-label="Permalink to this section"></a></h2>"##,
1189 example_markdown: "## Setup",
1190 status: Status::Emerging,
1191 since: "1",
1192 description: "Clickable permalink appended inside every author-written body heading that carries a slug id; links to the heading's `#`-fragment. The element is EMPTY — the `#` a reader sees is drawn by `site.css` as `::after` content, so selecting a heading never copies it. Not emitted for a display title: the auto-injected `moss-article-title` H1, a `:::hero` overlay heading, and a `:::grid` cell heading all get none.",
1193 },
1194 ComponentEntry {
1198 class: "moss-grid",
1199 kind: "container",
1200 parent: "",
1201 data_attrs: &[
1202 DataAttr {
1203 name: "data-width",
1204 values: &["body", "wide", "page", "screen"],
1205 default: "body",
1206 description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1207 },
1208 DataAttr {
1209 name: "data-columns",
1210 values: &["1", "2", "3", "4"],
1211 default: "",
1212 description: "Column count, from `:::grid N`. Note the responsive default: below 768px moss collapses `[data-columns]` to a single column, which is right for a grid of cards and wrong for a grid of short text lines. Re-assert `grid-template-columns` inside your own media query if yours is the latter. A ratio (`:::grid 2 1:2`) arrives as the custom property `--moss-grid-ratio` on the element, so it stays overridable — the collapse applies to ratio grids too.",
1213 },
1214 ],
1215 example_html: r#"<div class="moss-grid" data-width="wide">
1216 <div class="moss-grid-card">...</div>
1217</div>"#,
1218 example_markdown: ":::grid {cols=2}\nLeft cell\n+++\nRight cell\n:::\n",
1219 status: Status::Confirmed,
1220 since: "0",
1221 description: "Generic grid container (used by profiles, link previews, etc.). Modifier classes: `profiles`, `featured`, `no-cards`. v1 adds `data-width` (P9).",
1222 },
1223 ComponentEntry {
1224 class: "moss-grid-card",
1225 kind: "instance",
1226 parent: "moss-grid",
1227 data_attrs: &[
1228 DataAttr {
1229 name: "data-kind",
1230 values: &["link", "friend", "card"],
1231 default: "card",
1232 description: "v1 grid-card variant. Today expressed via co-emitted classes (`.link-card`, `.friend-card`, `.no-cards`); Phase 1c collapses to this `data-kind` attribute.",
1233 },
1234 DataAttr {
1235 name: "data-cover-color",
1236 values: &[],
1237 default: "",
1238 description: "Presence flag: emitted IFF the cell's first block is an image, alone in a paragraph or in a figure. The image's dominant colour arrives alongside it as `--moss-cover-color` in the element's `style`. moss paints nothing with it — the attribute exists so a theme can give a hand-built cell (cover, then the author's own text) the same colour band a collection card gets.",
1239 },
1240 ],
1241 example_html: r#"<a class="moss-grid-card" data-kind="link" href="...">...</a>"#,
1242 example_markdown: "",
1243 status: Status::Confirmed,
1244 since: "0",
1245 description: "Card instance inside `.moss-grid`. Today emits sibling classes `link-card` / `friend-card` / `no-cards`; v1 collapses to `data-kind`.",
1246 },
1247 ComponentEntry {
1248 class: "moss-gallery",
1249 kind: "container",
1250 parent: "",
1251 data_attrs: &[
1252 DataAttr {
1253 name: "data-width",
1254 values: &["body", "wide", "page", "screen"],
1255 default: "body",
1256 description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1257 },
1258 DataAttr {
1259 name: "data-columns",
1260 values: &[],
1261 default: "",
1262 description: "Column count, from `:::gallery N` — the author names it rather than moss inferring it. The **opposite** of `.moss-grid[data-columns]` on mobile: below 48rem the grid collapses to one column, while the gallery uses `auto-fill` to keep as many tracks as clear 88px. N becomes a maximum rather than a mandate, and a gallery that already fits stays at its authored count. Collapsing a wall of thumbnails to one column is wrong; collapsing prose cells is right.",
1263 },
1264 ],
1265 example_html: r#"<div class="moss-gallery" data-width="page">
1266 <div class="moss-gallery-item">...</div>
1267</div>"#,
1268 example_markdown: ":::gallery\nphoto.jpg\n:::\n",
1269 status: Status::Confirmed,
1270 since: "0",
1271 description: "Image gallery container. v1 adds `data-width` (P9).",
1272 },
1273 ComponentEntry {
1274 class: "moss-gallery-item",
1275 kind: "instance",
1276 parent: "moss-gallery",
1277 data_attrs: &[],
1278 example_html: r#"<div class="moss-gallery-item"><img src="..." /></div>"#,
1279 example_markdown: "",
1280 status: Status::Confirmed,
1281 since: "0",
1282 description: "Single image entry inside `.moss-gallery`.",
1283 },
1284 ComponentEntry {
1285 class: "moss-buttons",
1286 kind: "container",
1287 parent: "",
1288 data_attrs: &[
1289 DataAttr {
1290 name: "data-style",
1291 values: &["default", "inverted"],
1292 default: "default",
1293 description: "v1 button-row style. Theme authors target `.moss-buttons[data-style=...]`.",
1294 },
1295 ],
1296 example_html: r#"<div class="moss-buttons" data-style="inverted">
1297 <a class="moss-btn" href="...">Click</a>
1298</div>"#,
1299 example_markdown: ":::buttons\n[Get started](https://example.com)\n:::\n",
1300 status: Status::Confirmed,
1301 since: "0",
1302 description: "Container for a row of `.moss-btn` buttons. v1: the inverted variant is on `data-style=\"inverted\"`.",
1303 },
1304 ComponentEntry {
1308 class: "moss-btn",
1309 kind: "standalone",
1310 parent: "",
1311 data_attrs: &[
1312 DataAttr {
1313 name: "data-role",
1314 values: &["default", "primary", "secondary"],
1315 default: "default",
1316 description: "v1 button role. Theme authors target `.moss-btn[data-role=...]`.",
1317 },
1318 ],
1319 example_html: r#"<button class="moss-btn" data-role="primary">
1320 <span class="moss-btn__label">Submit</span>
1321</button>"#,
1322 example_markdown: "",
1323 status: Status::Confirmed,
1324 since: "0",
1325 description: "Generic button primitive. Role on `data-role` (v1).",
1326 },
1327 ComponentEntry {
1328 class: "moss-btn__label",
1329 kind: "instance",
1330 parent: "moss-btn",
1331 data_attrs: &[],
1332 example_html: r#"<span class="moss-btn__label">Submit</span>"#,
1333 example_markdown: "",
1334 status: Status::Confirmed,
1335 since: "0",
1336 description: "Label span inside `.moss-btn`.",
1337 },
1338 ComponentEntry {
1339 class: "moss-btn__check",
1340 kind: "instance",
1341 parent: "moss-btn",
1342 data_attrs: &[],
1343 example_html: r#"<span class="moss-btn__check">✓</span>"#,
1344 example_markdown: "",
1345 status: Status::Confirmed,
1346 since: "0",
1347 description: "Success checkmark slot inside `.moss-btn`.",
1348 },
1349 ComponentEntry {
1350 class: "moss-btn__spinner",
1351 kind: "instance",
1352 parent: "moss-btn",
1353 data_attrs: &[],
1354 example_html: r#"<span class="moss-btn__spinner"></span>"#,
1355 example_markdown: "",
1356 status: Status::Confirmed,
1357 since: "0",
1358 description: "Loading spinner slot inside `.moss-btn`.",
1359 },
1360 ComponentEntry {
1364 class: "moss-subscribe",
1365 kind: "standalone",
1366 parent: "",
1367 data_attrs: &[],
1368 example_html: r#"<div class="moss-subscribe">
1369 <form class="moss-subscribe-form">...</form>
1370</div>"#,
1371 example_markdown: ":::subscribe\n:::\n",
1372 status: Status::Confirmed,
1373 since: "0",
1374 description: "Newsletter subscribe block (auto-injected into footer when email channel configured).",
1375 },
1376 ComponentEntry {
1377 class: "moss-subscribe-form",
1378 kind: "instance",
1379 parent: "moss-subscribe",
1380 data_attrs: &[
1381 DataAttr {
1382 name: "data-position",
1383 values: &["inline", "apply"],
1384 default: "inline",
1385 description: "Placement/behavior variant. All moss-hosted subscribe forms are `inline` (the auto-injected footer form and the `:::subscribe` shortcode emit identical HTML — footer vs in-page styling keys on the `footer` ancestor in CSS, not this attribute). `apply` marks the `:::apply` form (terminal success, FormData body).",
1386 },
1387 DataAttr {
1388 name: "data-button-override",
1389 values: &["true"],
1390 default: "true",
1391 description: "Emitted only when the author overrides the button label (`:::subscribe{button=\"...\"}`). Signals subscribe.ts to leave the button label AND placeholder as authored instead of overwriting them with the language-default copy.",
1392 },
1393 DataAttr {
1394 name: "data-moss-hosted",
1395 values: &["true"],
1396 default: "true",
1397 description: "Marks moss-hosted (seta) forms hydrated by subscribe.ts. Absent on 3rd-party provider forms.",
1398 },
1399 DataAttr {
1400 name: "data-state",
1401 values: &["idle", "loading", "success", "error"],
1402 default: "idle",
1403 description: "Runtime submit state machine, driven by subscribe.ts. Emitted as `idle`; theme authors target `.moss-subscribe-form[data-state=...]`.",
1404 },
1405 DataAttr {
1406 name: "data-moss-pending-site",
1407 values: &["true"],
1408 default: "true",
1409 description: "Pre-first-publish pending wiring (`action=\"#\"`, no site_id yet). Hidden on published pages (body without `data-moss-preview`) via the email.css defense rule so a pending form never faces real readers.",
1410 },
1411 ],
1412 example_html: r#"<form class="moss-subscribe-form">...</form>"#,
1413 example_markdown: "",
1414 status: Status::Emerging,
1415 since: "0",
1416 description: "Form element inside `.moss-subscribe`.",
1417 },
1418 ComponentEntry {
1419 class: "moss-btn-slot",
1420 kind: "instance",
1421 parent: "moss-subscribe",
1422 data_attrs: &[],
1423 example_html: r#"<div class="moss-btn-slot"><button class="moss-btn">...</button></div>"#,
1424 example_markdown: "",
1425 status: Status::Emerging,
1426 since: "0",
1427 description: "Fixed-width slot wrapping a form's submit button; used by both the subscribe and comment forms to prevent layout shift across idle/loading/success states.",
1428 },
1429 ComponentEntry {
1430 class: "moss-subscribe-status",
1431 kind: "instance",
1432 parent: "moss-subscribe",
1433 data_attrs: &[],
1434 example_html: r#"<div class="moss-subscribe-status">
1435 <span class="moss-subscribe-status__icon"></span>
1436 Subscribed!
1437</div>"#,
1438 example_markdown: "",
1439 status: Status::Emerging,
1440 since: "0",
1441 description: "Status message shown after submit (success/error).",
1442 },
1443 ComponentEntry {
1444 class: "moss-subscribe-status__icon",
1445 kind: "instance",
1446 parent: "moss-subscribe-status",
1447 data_attrs: &[],
1448 example_html: r#"<span class="moss-subscribe-status__icon"></span>"#,
1449 example_markdown: "",
1450 status: Status::Emerging,
1451 since: "0",
1452 description: "Icon slot inside `.moss-subscribe-status`.",
1453 },
1454 ComponentEntry {
1455 class: "moss-subscribe-landing",
1456 kind: "standalone",
1457 parent: "",
1458 data_attrs: &[],
1459 example_html: r#"<section class="moss-subscribe-landing">...</section>"#,
1460 example_markdown: "",
1461 status: Status::Emerging,
1462 since: "0",
1463 description: "Standalone subscribe landing page surface (larger variant).",
1464 },
1465 ComponentEntry {
1469 class: "moss-apply",
1470 kind: "standalone",
1471 parent: "",
1472 data_attrs: &[],
1473 example_html: r#"<div class="moss-apply" data-state="idle">
1474 <form class="moss-subscribe-form moss-apply-form">...</form>
1475</div>"#,
1476 example_markdown: ":::apply\n:::\n",
1477 status: Status::Emerging,
1478 since: "0",
1479 description: "Apply / membership-request form block (:::apply shortcode).",
1480 },
1481 ComponentEntry {
1482 class: "moss-apply-form",
1483 kind: "instance",
1484 parent: "moss-apply",
1485 data_attrs: &[
1486 DataAttr {
1487 name: "data-position",
1488 values: &["apply"],
1489 default: "apply",
1490 description: "Position variant; always `apply` for this form. Drives CSS layout in email.css.",
1491 },
1492 DataAttr {
1493 name: "data-revert",
1494 values: &["false"],
1495 default: "false",
1496 description: "When `false`, success is terminal (no auto-revert). subscribe.ts reads this.",
1497 },
1498 ],
1499 example_html: r#"<form class="moss-subscribe-form moss-apply-form" data-position="apply" data-revert="false">...</form>"#,
1500 example_markdown: "",
1501 status: Status::Emerging,
1502 since: "0",
1503 description: "Form element inside `.moss-apply`. Also carries `.moss-subscribe-form` so subscribe.ts hydrates it.",
1504 },
1505 ComponentEntry {
1506 class: "moss-apply-matters",
1507 kind: "instance",
1508 parent: "moss-apply",
1509 data_attrs: &[],
1510 example_html: r#"<input type="text" name="matters" class="moss-input moss-apply-matters">"#,
1511 example_markdown: "",
1512 status: Status::Emerging,
1513 since: "0",
1514 description: "Second apply-form input inside `.moss-apply-form` — a Matters username OR a one-line pitch (placeholder-only, no visible label).",
1515 },
1516 ComponentEntry {
1517 class: "moss-apply-hp",
1518 kind: "instance",
1519 parent: "moss-apply",
1520 data_attrs: &[],
1521 example_html: r#"<input type="text" name="website" class="moss-apply-hp" tabindex="-1" aria-hidden="true">"#,
1522 example_markdown: "",
1523 status: Status::Emerging,
1524 since: "0",
1525 description: "Honeypot field (off-screen) inside `.moss-apply-form`. Bots fill it; humans don't.",
1526 },
1527 ComponentEntry {
1528 class: "moss-apply-status",
1529 kind: "instance",
1530 parent: "moss-apply",
1531 data_attrs: &[],
1532 example_html: r#"<div class="moss-subscribe-status moss-apply-status" aria-live="polite">...</div>"#,
1533 example_markdown: "",
1534 status: Status::Emerging,
1535 since: "0",
1536 description: "Status region inside `.moss-apply-form` (also carries `.moss-subscribe-status`).",
1537 },
1538 ComponentEntry {
1539 class: "moss-apply-helper",
1540 kind: "instance",
1541 parent: "moss-apply",
1542 data_attrs: &[],
1543 example_html: r#"<p class="moss-apply-helper" id="moss-apply-email-help">用于获取邀请及免费托管服务</p>"#,
1544 example_markdown: "",
1545 status: Status::Emerging,
1546 since: "0",
1547 description: "Helper text line beneath each field in `.moss-apply-form` (referenced by the field's aria-describedby). Internal — not part of the public component contract.",
1548 },
1549 ComponentEntry {
1553 class: "moss-series-nav",
1554 kind: "standalone",
1555 parent: "",
1556 data_attrs: &[],
1557 example_html: r#"<nav class="moss-series-nav">
1558 <div class="moss-series-nav-links">...</div>
1559</nav>"#,
1560 example_markdown: "",
1561 status: Status::Confirmed,
1562 since: "0",
1563 description: "Series navigation bar (prev/next/collection) on series pages.",
1564 },
1565 ComponentEntry {
1566 class: "moss-series-nav-links",
1567 kind: "instance",
1568 parent: "moss-series-nav",
1569 data_attrs: &[],
1570 example_html: r#"<div class="moss-series-nav-links">...</div>"#,
1571 example_markdown: "",
1572 status: Status::Confirmed,
1573 since: "0",
1574 description: "Row holding prev/next links in series nav.",
1575 },
1576 ComponentEntry {
1577 class: "moss-series-nav-link",
1578 kind: "instance",
1579 parent: "moss-series-nav",
1580 data_attrs: &[],
1581 example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1582 example_markdown: "",
1583 status: Status::Confirmed,
1584 since: "0",
1585 description: "Individual link inside series nav. Modifiers: `moss-series-nav-prev`, `moss-series-nav-next`, `empty` (placeholder).",
1586 },
1587 ComponentEntry {
1588 class: "moss-series-nav-prev",
1589 kind: "instance",
1590 parent: "moss-series-nav",
1591 data_attrs: &[],
1592 example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1593 example_markdown: "",
1594 status: Status::Confirmed,
1595 since: "0",
1596 description: "Previous-page modifier on a series nav link.",
1597 },
1598 ComponentEntry {
1599 class: "moss-series-nav-next",
1600 kind: "instance",
1601 parent: "moss-series-nav",
1602 data_attrs: &[],
1603 example_html: r#"<a class="moss-series-nav-link moss-series-nav-next" href="...">...</a>"#,
1604 example_markdown: "",
1605 status: Status::Confirmed,
1606 since: "0",
1607 description: "Next-page modifier on a series nav link.",
1608 },
1609 ComponentEntry {
1610 class: "moss-series-nav-arrow",
1611 kind: "instance",
1612 parent: "moss-series-nav",
1613 data_attrs: &[],
1614 example_html: r#"<span class="moss-series-nav-arrow">→</span>"#,
1615 example_markdown: "",
1616 status: Status::Confirmed,
1617 since: "0",
1618 description: "Arrow glyph inside a series-nav link.",
1619 },
1620 ComponentEntry {
1621 class: "moss-series-nav-title",
1622 kind: "instance",
1623 parent: "moss-series-nav",
1624 data_attrs: &[],
1625 example_html: r#"<span class="moss-series-nav-title">Next page title</span>"#,
1626 example_markdown: "",
1627 status: Status::Confirmed,
1628 since: "0",
1629 description: "Title text of the destination page in a series-nav link.",
1630 },
1631 ComponentEntry {
1632 class: "moss-series-nav-collection",
1633 kind: "instance",
1634 parent: "moss-series-nav",
1635 data_attrs: &[],
1636 example_html: r#"<div class="moss-series-nav-collection">...</div>"#,
1637 example_markdown: "",
1638 status: Status::Confirmed,
1639 since: "0",
1640 description: "Collection-listing slot in series nav (sibling pages).",
1641 },
1642 ComponentEntry {
1643 class: "moss-series-nav-collection-row",
1644 kind: "instance",
1645 parent: "moss-series-nav-collection",
1646 data_attrs: &[],
1647 example_html: r#"<div class="moss-series-nav-collection-row">...</div>"#,
1648 example_markdown: "",
1649 status: Status::Confirmed,
1650 since: "0",
1651 description: "Row inside the collection listing of series nav.",
1652 },
1653 ComponentEntry {
1654 class: "moss-series-nav-position",
1655 kind: "instance",
1656 parent: "moss-series-nav-collection-row",
1657 data_attrs: &[],
1658 example_html: r#"<span class="moss-series-nav-position">2 of 3</span>"#,
1659 example_markdown: "",
1660 status: Status::Confirmed,
1661 since: "0",
1662 description: "Where this page sits in its series — \"2 of 3\" / 「第 2 篇,共 3 篇」. Counts only the pages still in the reading order, so a page that stepped out with `series: false` is not in the total. Omitted when the folder holds a single page.",
1663 },
1664 ComponentEntry {
1668 class: "moss-collection-cover",
1669 kind: "standalone",
1670 parent: "",
1671 data_attrs: &[],
1672 example_html: r#"<section class="moss-collection-cover">
1673 <div class="moss-collection-cover-row">...</div>
1674</section>"#,
1675 example_markdown: "",
1676 status: Status::Emerging,
1677 since: "0",
1678 description: "Header surface on a collection landing page.",
1679 },
1680 ComponentEntry {
1681 class: "moss-collection-cover-row",
1682 kind: "instance",
1683 parent: "moss-collection-cover",
1684 data_attrs: &[],
1685 example_html: r#"<div class="moss-collection-cover-row">...</div>"#,
1686 example_markdown: "",
1687 status: Status::Emerging,
1688 since: "0",
1689 description: "Row inside `.moss-collection-cover`.",
1690 },
1691 ComponentEntry {
1692 class: "moss-collection-cover-body",
1693 kind: "instance",
1694 parent: "moss-collection-cover",
1695 data_attrs: &[],
1696 example_html: r#"<div class="moss-collection-cover-body">...</div>"#,
1697 example_markdown: "",
1698 status: Status::Emerging,
1699 since: "0",
1700 description: "Body content slot inside `.moss-collection-cover`.",
1701 },
1702 ComponentEntry {
1706 class: "moss-input",
1707 kind: "standalone",
1708 parent: "",
1709 data_attrs: &[],
1710 example_html: r#"<input class="moss-input" type="email" />"#,
1711 example_markdown: "",
1712 status: Status::Confirmed,
1713 since: "0",
1714 description: "Generic form input primitive.",
1715 },
1716 ComponentEntry {
1717 class: "moss-field",
1718 kind: "container",
1719 parent: "",
1720 data_attrs: &[],
1721 example_html: r#"<div class="moss-field">
1722 <label class="moss-label">Email</label>
1723 <input class="moss-input" />
1724</div>"#,
1725 example_markdown: "",
1726 status: Status::Confirmed,
1727 since: "0",
1728 description: "Form field group (label + input). Modifier `--inline` for horizontal layout.",
1729 },
1730 ComponentEntry {
1731 class: "moss-label",
1732 kind: "instance",
1733 parent: "moss-field",
1734 data_attrs: &[],
1735 example_html: r#"<label class="moss-label">Email</label>"#,
1736 example_markdown: "",
1737 status: Status::Confirmed,
1738 since: "0",
1739 description: "Label primitive for `.moss-field`. Modifier `--small` for compact form.",
1740 },
1741 ComponentEntry {
1742 class: "moss-link",
1743 kind: "standalone",
1744 parent: "",
1745 data_attrs: &[],
1746 example_html: r#"<a class="moss-link" href="...">Click me</a>"#,
1747 example_markdown: "",
1748 status: Status::Confirmed,
1749 since: "0",
1750 description: "Inline-link primitive (resets `<button>` chrome too). Use `--subtle` for muted variant.",
1751 },
1752 ComponentEntry {
1753 class: "moss-field--inline",
1754 kind: "instance",
1755 parent: "moss-field",
1756 data_attrs: &[],
1757 example_html: r#"<div class="moss-field moss-field--inline">
1758 <label class="moss-label">Email</label>
1759 <input class="moss-input" />
1760</div>"#,
1761 example_markdown: "",
1762 status: Status::Confirmed,
1763 since: "0",
1764 description: "BEM modifier on `.moss-field` for horizontal label+input layout (used by settings UI primitives).",
1765 },
1766 ComponentEntry {
1767 class: "moss-label--small",
1768 kind: "instance",
1769 parent: "moss-label",
1770 data_attrs: &[],
1771 example_html: r#"<label class="moss-label moss-label--small">Compact label</label>"#,
1772 example_markdown: "",
1773 status: Status::Confirmed,
1774 since: "0",
1775 description: "BEM modifier on `.moss-label` for compact form (used by services settings rows).",
1776 },
1777 ComponentEntry {
1778 class: "moss-info-grid",
1779 kind: "container",
1780 parent: "",
1781 data_attrs: &[],
1782 example_html: r#"<div class="moss-info-grid">
1783 <div class="moss-field moss-field--inline">...</div>
1784 <div class="moss-field moss-field--inline">...</div>
1785</div>"#,
1786 example_markdown: "",
1787 status: Status::Emerging,
1788 since: "0",
1789 description: "Two-column aligned label+value rows (CSS grid with `display: contents` children). Used by the deployment settings panel; ships in the default theme so authors can reuse the layout.",
1790 },
1791 ComponentEntry {
1792 class: "moss-row",
1793 kind: "container",
1794 parent: "",
1795 data_attrs: &[],
1796 example_html: r#"<div class="moss-row">
1797 <div class="moss-field">...</div>
1798 <div class="moss-field">...</div>
1799</div>"#,
1800 example_markdown: "",
1801 status: Status::Emerging,
1802 since: "0",
1803 description: "Horizontal flex row of equal-flex `.moss-field` children. Form-row layout helper shipped in the default theme.",
1804 },
1805 ComponentEntry {
1806 class: "moss-input-feedback",
1807 kind: "instance",
1808 parent: "moss-field",
1809 data_attrs: &[],
1810 example_html: r#"<span class="moss-input-feedback">Saving…</span>"#,
1811 example_markdown: "",
1812 status: Status::Emerging,
1813 since: "0",
1814 description: "Auto-save status hint slot under `.moss-field`. Three state modifiers: `--success`, `--error`, `--fade-out`.",
1815 },
1816 ComponentEntry {
1817 class: "moss-input-feedback--success",
1818 kind: "instance",
1819 parent: "moss-input-feedback",
1820 data_attrs: &[],
1821 example_html: r#"<span class="moss-input-feedback moss-input-feedback--success">Saved</span>"#,
1822 example_markdown: "",
1823 status: Status::Emerging,
1824 since: "0",
1825 description: "Success state modifier on `.moss-input-feedback`.",
1826 },
1827 ComponentEntry {
1828 class: "moss-input-feedback--error",
1829 kind: "instance",
1830 parent: "moss-input-feedback",
1831 data_attrs: &[],
1832 example_html: r#"<span class="moss-input-feedback moss-input-feedback--error">Failed to save</span>"#,
1833 example_markdown: "",
1834 status: Status::Emerging,
1835 since: "0",
1836 description: "Error state modifier on `.moss-input-feedback`.",
1837 },
1838 ComponentEntry {
1839 class: "moss-input-feedback--fade-out",
1840 kind: "instance",
1841 parent: "moss-input-feedback",
1842 data_attrs: &[],
1843 example_html: r#"<span class="moss-input-feedback moss-input-feedback--success moss-input-feedback--fade-out">Saved</span>"#,
1844 example_markdown: "",
1845 status: Status::Emerging,
1846 since: "0",
1847 description: "Transient fade-out modifier on `.moss-input-feedback` (applied after a success message to dismiss it).",
1848 },
1849 ComponentEntry {
1853 class: "moss-comments",
1854 kind: "standalone",
1855 parent: "",
1856 data_attrs: &[],
1857 example_html: r#"<section class="moss-comments">...</section>"#,
1858 example_markdown: "",
1859 status: Status::Confirmed,
1860 since: "0",
1861 description: "Comments surface (per-site SQLite backend or Artalk legacy).",
1862 },
1863 ComponentEntry {
1864 class: "moss-service-inactive",
1865 kind: "instance",
1866 parent: "",
1867 data_attrs: &[],
1868 example_html: r#"<section class="moss-comments moss-service-inactive">...</section>"#,
1869 example_markdown: "",
1870 status: Status::Confirmed,
1871 since: "0",
1872 description: "Co-class applied to `.moss-comments` and `.moss-subscribe-form` when the backing service is not configured. Hidden by default in published sites and revealed inside the preview chrome so authors can see the inactive surface during editing.",
1873 },
1874 ComponentEntry {
1878 class: "moss-preview-popup",
1879 kind: "chrome",
1880 parent: "",
1881 data_attrs: &[],
1882 example_html: r#"<div class="moss-preview-popup" role="tooltip" aria-live="polite">
1883 <strong class="moss-preview-title">...</strong>
1884 <p class="moss-preview-desc">...</p>
1885 <p class="moss-preview-text">...</p>
1886</div>"#,
1887 example_markdown: "",
1888 status: Status::Confirmed,
1889 since: "0",
1890 description: "Floating link-preview popover injected at `document.body` level by the runtime `preview.js`. Fetches `/_moss/previews.json` and renders a hover card with title, description, and excerpt for internal links.",
1891 },
1892 ComponentEntry {
1893 class: "moss-preview-title",
1894 kind: "instance",
1895 parent: "moss-preview-popup",
1896 data_attrs: &[],
1897 example_html: r#"<strong class="moss-preview-title">Article title</strong>"#,
1898 example_markdown: "",
1899 status: Status::Confirmed,
1900 since: "0",
1901 description: "Title slot inside `.moss-preview-popup`.",
1902 },
1903 ComponentEntry {
1904 class: "moss-preview-desc",
1905 kind: "instance",
1906 parent: "moss-preview-popup",
1907 data_attrs: &[],
1908 example_html: r#"<p class="moss-preview-desc">Short description</p>"#,
1909 example_markdown: "",
1910 status: Status::Confirmed,
1911 since: "0",
1912 description: "Description slot inside `.moss-preview-popup` (from frontmatter `description`).",
1913 },
1914 ComponentEntry {
1915 class: "moss-preview-text",
1916 kind: "instance",
1917 parent: "moss-preview-popup",
1918 data_attrs: &[],
1919 example_html: r#"<p class="moss-preview-text">Excerpt of the linked article…</p>"#,
1920 example_markdown: "",
1921 status: Status::Confirmed,
1922 since: "0",
1923 description: "Excerpt slot inside `.moss-preview-popup` (auto-extracted from the linked article body).",
1924 },
1925 ComponentEntry {
1926 class: "moss-colophon",
1927 kind: "chrome",
1928 parent: "",
1929 data_attrs: &[],
1930 example_html: r#"<div class="moss-colophon">
1931 <a href="https://mosspub.com">
1932 <svg class="moss-colophon-icon"></svg>
1933 <span class="moss-colophon-label">Published with moss</span>
1934 </a>
1935</div>"#,
1936 example_markdown: "",
1937 status: Status::Confirmed,
1938 since: "0",
1939 description: "Footer colophon credit appended by moss. Shows the moss mark alone at rest; the wording fades in beneath it on hover or keyboard focus, without moving the mark.",
1940 },
1941 ComponentEntry {
1942 class: "moss-colophon-icon",
1943 kind: "instance",
1944 parent: "moss-colophon",
1945 data_attrs: &[],
1946 example_html: r#"<svg class="moss-colophon-icon"></svg>"#,
1947 example_markdown: "",
1948 status: Status::Confirmed,
1949 since: "0",
1950 description: "The moss mark inside `.moss-colophon`. Decorative (`aria-hidden`) — `.moss-colophon-label` carries the accessible name.",
1951 },
1952 ComponentEntry {
1953 class: "moss-colophon-label",
1954 kind: "instance",
1955 parent: "moss-colophon",
1956 data_attrs: &[],
1957 example_html: r#"<span class="moss-colophon-label">Published with moss</span>"#,
1958 example_markdown: "",
1959 status: Status::Confirmed,
1960 since: "0",
1961 description: "Localized attribution wording inside `.moss-colophon`. Transparent at rest and faded in on hover/focus, positioned out of flow beneath the mark so the reveal shifts nothing — it stays in the DOM because it is the link's accessible name.",
1962 },
1963 ComponentEntry {
1964 class: "moss-shell-frame",
1965 kind: "chrome",
1966 parent: "",
1967 data_attrs: &[],
1968 example_html: r#"<div class="moss-shell-frame">...</div>"#,
1969 example_markdown: "",
1970 status: Status::Emerging,
1971 since: "0",
1972 description: "App-shell frame surface (preview chrome).",
1973 },
1974 ComponentEntry {
1975 class: "moss-mobile-frame",
1976 kind: "chrome",
1977 parent: "moss-shell-frame",
1978 data_attrs: &[],
1979 example_html: r#"<html class="moss-shell-frame moss-mobile-frame">...</html>"#,
1980 example_markdown: "",
1981 status: Status::Emerging,
1982 since: "0",
1983 description: "Runtime marker the preview bridge adds to `<html>` when the shell is in mobile device-preview mode. Since ADR-039 the shell owns chrome clearance by insetting the preview iframe, so no CSS keys off this class and it currently has no effect; it is retained as a revert path and may be removed.",
1984 },
1985 ComponentEntry {
1986 class: "main-nav",
1987 kind: "chrome",
1988 parent: "",
1989 data_attrs: &[],
1990 example_html: r#"<nav class="main-nav container">...</nav>"#,
1991 example_markdown: "",
1992 status: Status::Confirmed,
1993 since: "0",
1994 description: "Top site navigation bar. Legacy non-`moss-` prefix kept for theme parity.",
1995 },
1996 ComponentEntry {
2008 class: "date-line",
2009 kind: "chrome",
2010 parent: "",
2011 data_attrs: &[],
2012 example_html: r#"<div class="date-line"><span class="date">March 3, 2026</span><div class="font-anchor">...</div></div>"#,
2013 example_markdown: "",
2014 status: Status::Confirmed,
2015 since: "0",
2016 description: "Byline row under an article title: the publication date on the left, the reading-size control on the right. Emitted only when the page has a `date`.",
2017 },
2018 ComponentEntry {
2019 class: "date",
2020 kind: "instance",
2021 parent: "date-line",
2022 data_attrs: &[],
2023 example_html: r#"<span class="date">March 3, 2026</span>"#,
2024 example_markdown: "",
2025 status: Status::Confirmed,
2026 since: "0",
2027 description: "The formatted publication date inside `.date-line`. Text is localized to the page's language.",
2028 },
2029 ComponentEntry {
2030 class: "moss-byline",
2031 kind: "container",
2032 parent: "",
2033 data_attrs: &[],
2034 example_html: r#"<div class="moss-byline"><div class="moss-byline-row">作者 糜緒洋</div><div class="moss-byline-row">編輯 謝丁</div></div>"#,
2035 example_markdown: "",
2036 status: Status::Confirmed,
2037 since: "1",
2038 description: "Credit block under the page title, below `.date-line` when there is one. Emitted from the `byline` frontmatter field on every page kind — articles, folder indexes, the homepage and plain pages alike — one `.moss-byline-row` per authored line. On a page moss gives no title of its own (the homepage, a `home: true` folder page, a plain page) it sits under the author's own opening `<h1>`, or at the top of the page content when the body has none. Absent when the field is.",
2039 },
2040 ComponentEntry {
2041 class: "moss-byline-row",
2042 kind: "instance",
2043 parent: "moss-byline",
2044 data_attrs: &[],
2045 example_html: r#"<div class="moss-byline-row">首發媒體 <a href="https://theinitium.com/a">端傳媒</a></div>"#,
2046 example_markdown: "",
2047 status: Status::Confirmed,
2048 since: "1",
2049 description: "One credit line. Its content is the author's text rendered as inline markdown, so a row may contain links or emphasis. moss does not know which part is a role and which is a name — style the whole row.",
2050 },
2051 ComponentEntry {
2052 class: "moss-article-colophon",
2053 kind: "container",
2054 parent: "",
2055 data_attrs: &[],
2056 example_html: r#"<div class="moss-article-colophon"><div class="moss-article-colophon-row">首發媒體 <a href="https://theinitium.com/a">端傳媒</a></div></div>"#,
2057 example_markdown: "",
2058 status: Status::Confirmed,
2059 since: "1",
2060 description: "Credit block at the FOOT of the page, emitted from the `colophon` frontmatter field — where the piece first ran, contributor biographies, production credits. Same rows as `.moss-byline`, different end of the page. Emitted on every page kind: inside `<article>` on an article page, and last in the page content everywhere else — after the children listing on a folder index or the homepage — where the enclosing element is not an `<article>` despite the class name. Unrelated to `.review-colophon`, which is the review feature's book card.",
2061 },
2062 ComponentEntry {
2063 class: "moss-article-colophon-row",
2064 kind: "instance",
2065 parent: "moss-article-colophon",
2066 data_attrs: &[],
2067 example_html: r#"<div class="moss-article-colophon-row">封面 基輔米迦勒修道院門口的陣亡將士紀念牆(拍攝:糜緒洋)</div>"#,
2068 example_markdown: "",
2069 status: Status::Confirmed,
2070 since: "1",
2071 description: "One foot-credit line, rendered as inline markdown exactly like `.moss-byline-row`.",
2072 },
2073 ComponentEntry {
2074 class: "site-name",
2075 kind: "instance",
2076 parent: "main-nav",
2077 data_attrs: &[],
2078 example_html: r#"<a href="/" class="site-name">在場</a>"#,
2079 example_markdown: "",
2080 status: Status::Confirmed,
2081 since: "0",
2082 description: "The site title link at the left of the nav bar. On a non-home page the same slot may instead carry `.breadcrumb-segment`.",
2083 },
2084 ComponentEntry {
2085 class: "breadcrumb-segment",
2086 kind: "instance",
2087 parent: "main-nav",
2088 data_attrs: &[],
2089 example_html: r#"<a href="/awards/" class="breadcrumb-segment">獎項</a>"#,
2090 example_markdown: "",
2091 status: Status::Confirmed,
2092 since: "0",
2093 description: "One ancestor link in the nav-left breadcrumb trail, used in place of `.site-name` once the page is below the site root.",
2094 },
2095 ComponentEntry {
2096 class: "nav-icons",
2097 kind: "chrome",
2098 parent: "main-nav",
2099 data_attrs: &[],
2100 example_html: r#"<div class="nav-icons">...</div>"#,
2101 example_markdown: "",
2102 status: Status::Confirmed,
2103 since: "0",
2104 description: "Right-hand icon cluster in the nav bar (search, theme toggle, and similar). Stationary chrome, present whether or not the site has nav links.",
2105 },
2106 ComponentEntry {
2118 class: "nav-left",
2119 kind: "chrome",
2120 parent: "main-nav",
2121 data_attrs: &[],
2122 example_html: r#"<div class="nav-left"><a href="/" class="site-name">在場</a></div>"#,
2123 example_markdown: "",
2124 status: Status::Confirmed,
2125 since: "0",
2126 description: "Left group of the nav bar. Holds either `.site-name` or the breadcrumb trail, never both.",
2127 },
2128 ComponentEntry {
2129 class: "nav-right",
2130 kind: "chrome",
2131 parent: "main-nav",
2132 data_attrs: &[],
2133 example_html: r#"<div class="nav-right">…hamburger, .nav-links, .nav-icons…</div>"#,
2134 example_markdown: "",
2135 status: Status::Confirmed,
2136 since: "0",
2137 description: "Right group of the nav bar: the mobile menu button, the nav links, and the icon cluster, in that order.",
2138 },
2139 ComponentEntry {
2140 class: "nav-links",
2141 kind: "chrome",
2142 parent: "nav-right",
2143 data_attrs: &[],
2144 example_html: r#"<div class="nav-links"><a href="/about/" class="active">關於</a>…</div>"#,
2145 example_markdown: "",
2146 status: Status::Confirmed,
2147 since: "0",
2148 description: "The nav link list. The link for the page currently being viewed additionally carries the bare class `active` — style `.nav-links .active`, not a `moss-` class.",
2149 },
2150 ComponentEntry {
2151 class: "site-logo",
2152 kind: "instance",
2153 parent: "site-name",
2154 data_attrs: &[],
2155 example_html: r#"<img class="site-logo" src="…" alt="" aria-hidden="true">"#,
2156 example_markdown: "",
2157 status: Status::Confirmed,
2158 since: "0",
2159 description: "Optional logo image inside the site-name link. Decorative by construction (`alt=\"\"` + `aria-hidden`), because the adjacent text already names the site.",
2160 },
2161 ComponentEntry {
2162 class: "breadcrumb-label",
2163 kind: "instance",
2164 parent: "nav-left",
2165 data_attrs: &[],
2166 example_html: r#"<span class="breadcrumb-label">獎項</span>"#,
2167 example_markdown: "",
2168 status: Status::Confirmed,
2169 since: "0",
2170 description: "The final, non-linked breadcrumb segment — the page you are on. `.breadcrumb-segment` is the linked form for ancestors.",
2171 },
2172 ComponentEntry {
2173 class: "breadcrumb-separator",
2174 kind: "instance",
2175 parent: "nav-left",
2176 data_attrs: &[],
2177 example_html: r#"<span class="breadcrumb-separator">/</span>"#,
2178 example_markdown: "",
2179 status: Status::Confirmed,
2180 since: "0",
2181 description: "The `/` between breadcrumb segments. Restyle or hide this rather than trying to remove it from the markup.",
2182 },
2183 ComponentEntry {
2184 class: "mobile-menu-button",
2185 kind: "chrome",
2186 parent: "nav-right",
2187 data_attrs: &[],
2188 example_html: r#"<button class="mobile-menu-button" aria-label="…"><svg>…</svg></button>"#,
2189 example_markdown: "",
2190 status: Status::Confirmed,
2191 since: "0",
2192 description: "The hamburger. Emitted on every page and hidden by media query above the mobile breakpoint — it is not conditionally rendered, so a rule that shows it always will.",
2193 },
2194 ComponentEntry {
2195 class: "nav-search-btn",
2196 kind: "instance",
2197 parent: "nav-icons",
2198 data_attrs: &[],
2199 example_html: r#"<button class="nav-search-btn" type="button" aria-label="…"><svg class="search-icon">…</svg></button>"#,
2200 example_markdown: "",
2201 status: Status::Confirmed,
2202 since: "0",
2203 description: "Search button in the nav icon cluster. Its glyph is `.search-icon`.",
2204 },
2205 ComponentEntry {
2206 class: "search-icon",
2207 kind: "instance",
2208 parent: "nav-search-btn",
2209 data_attrs: &[],
2210 example_html: r#"<svg class="search-icon" aria-hidden="true" width="1em" height="1em">…</svg>"#,
2211 example_markdown: "",
2212 status: Status::Confirmed,
2213 since: "0",
2214 description: "The magnifier glyph. Sized in `em` and stroked with `currentColor`, so it follows the button's font-size and color rather than needing its own rule.",
2215 },
2216 ComponentEntry {
2217 class: "nav-theme-btn",
2218 kind: "instance",
2219 parent: "nav-icons",
2220 data_attrs: &[],
2221 example_html: r#"<button class="nav-theme-btn" type="button" aria-label="…"><svg class="theme-toggle-icon">…</svg></button>"#,
2222 example_markdown: "",
2223 status: Status::Confirmed,
2224 since: "0",
2225 description: "Light/dark toggle in the nav icon cluster. Its glyph is `.theme-toggle-icon`.",
2226 },
2227 ComponentEntry {
2228 class: "theme-toggle-icon",
2229 kind: "instance",
2230 parent: "nav-theme-btn",
2231 data_attrs: &[],
2232 example_html: r#"<svg class="theme-toggle-icon" aria-hidden="true" width="1em" height="1em">…</svg>"#,
2233 example_markdown: "",
2234 status: Status::Confirmed,
2235 since: "0",
2236 description: "The sun/moon glyph. One SVG whose clip path animates between states — restyle it, but do not expect two separate icons to swap.",
2237 },
2238 ComponentEntry {
2239 class: "nav-lang-toggle",
2240 kind: "chrome",
2241 parent: "nav-icons",
2242 data_attrs: &[],
2243 example_html: r#"<div class="nav-lang-toggle" aria-label="…"><span class="nav-lang-current">繁</span><a href="/en/" class="nav-lang-link" hreflang="en">EN</a></div>"#,
2244 example_markdown: "",
2245 status: Status::Confirmed,
2246 since: "0",
2247 description: "The language switcher. Present only when the site has more than one edition among the three the switcher resolves (English, Simplified Chinese, Traditional Chinese) — a `ja/` or `fr/` tree publishes but adds no entry here.",
2248 },
2249 ComponentEntry {
2250 class: "nav-lang-current",
2251 kind: "instance",
2252 parent: "nav-lang-toggle",
2253 data_attrs: &[],
2254 example_html: r#"<span class="nav-lang-current">繁</span>"#,
2255 example_markdown: "",
2256 status: Status::Confirmed,
2257 since: "0",
2258 description: "The edition being viewed, as inert text rather than a link — style the current-language affordance here.",
2259 },
2260 ComponentEntry {
2261 class: "nav-lang-link",
2262 kind: "instance",
2263 parent: "nav-lang-toggle",
2264 data_attrs: &[],
2265 example_html: r#"<a href="/en/" class="nav-lang-link" hreflang="en">EN</a>"#,
2266 example_markdown: "",
2267 status: Status::Confirmed,
2268 since: "0",
2269 description: "A link to another edition of the same page. Carries `hreflang`, so `[hreflang=\"en\"]` is a stable hook for per-language styling.",
2270 },
2271 ComponentEntry {
2279 class: "moss-nav-island",
2280 kind: "chrome",
2281 parent: "",
2282 data_attrs: &[DataAttr {
2283 name: "data-shown",
2284 values: &["false", "true"],
2285 default: "false",
2286 description: "Whether the island is currently revealed. Written by the site runtime; ABSENT in the emitted HTML, which is what keeps the island invisible with JavaScript off.",
2287 }],
2288 example_html: r#"<div class="moss-nav-island" data-shown="true"><div class="moss-nav-island-bar">…</div></div>"#,
2289 example_markdown: "",
2290 status: Status::Emerging,
2291 since: "0",
2292 description: "Floating navigation island: a one-line bar, aligned to the text column, revealed on scroll-up once the masthead has left the screen. Emitted on any page with a breadcrumb trail, but it only ever shows where it can take the reader somewhere — a trail of three or more crumbs (down the tree) or a page with headings (the sections panel). On a top-level page with neither, the markup stays dormant (ADR-049 §10). Turn it off site-wide with `[site].floating_nav = false` or `--moss-nav-island-display: none`.",
2293 },
2294 ComponentEntry {
2295 class: "moss-nav-island-bar",
2296 kind: "instance",
2297 parent: "moss-nav-island",
2298 data_attrs: &[],
2299 example_html: r#"<div class="moss-nav-island-bar">…trail, actions, progress…</div>"#,
2300 example_markdown: "",
2301 status: Status::Emerging,
2302 since: "0",
2303 description: "The visible rounded bar. Its width follows `--moss-nav-width`/`--moss-content-width`, so it lines up with the article text rather than with the window.",
2304 },
2305 ComponentEntry {
2306 class: "moss-nav-island-trail",
2307 kind: "instance",
2308 parent: "moss-nav-island-bar",
2309 data_attrs: &[],
2310 example_html: r#"<nav class="moss-nav-island-trail" aria-label="Breadcrumb">…</nav>"#,
2311 example_markdown: "",
2312 status: Status::Emerging,
2313 since: "0",
2314 description: "The breadcrumb inside the island. Same segment classes as the masthead's, plus the current page as a final crumb. It never wraps: ancestors fold into `.moss-nav-island-more` until the row fits.",
2315 },
2316 ComponentEntry {
2317 class: "moss-nav-island-current",
2318 kind: "instance",
2319 parent: "moss-nav-island-trail",
2320 data_attrs: &[],
2321 example_html: r#"<span class="breadcrumb-segment moss-nav-island-current" aria-current="page">末代女礦工</span>"#,
2322 example_markdown: "",
2323 status: Status::Emerging,
2324 since: "0",
2325 description: "The page you are on, as the trail's last crumb. The only crumb permitted to truncate — an ancestor either fits whole or folds away.",
2326 },
2327 ComponentEntry {
2328 class: "moss-nav-island-more",
2329 kind: "instance",
2330 parent: "moss-nav-island-trail",
2331 data_attrs: &[],
2332 example_html: r#"<button class="moss-nav-island-more" aria-expanded="false">…</button>"#,
2333 example_markdown: "",
2334 status: Status::Emerging,
2335 since: "0",
2336 description: "Stands in for the ancestor levels the trail had to drop. Opens the levels menu on click; names them on hover via `data-tooltip`. Never opens on hover — a touch device has none, and that is the width where folding happens.",
2337 },
2338 ComponentEntry {
2339 class: "moss-nav-island-actions",
2340 kind: "instance",
2341 parent: "moss-nav-island-bar",
2342 data_attrs: &[],
2343 example_html: r#"<span class="moss-nav-island-actions">…</span>"#,
2344 example_markdown: "",
2345 status: Status::Emerging,
2346 since: "0",
2347 description: "Button cluster at the island's end edge. Holds the sections button only — theme, language and search stay in the masthead.",
2348 },
2349 ComponentEntry {
2350 class: "moss-nav-island-sections",
2351 kind: "instance",
2352 parent: "moss-nav-island-actions",
2353 data_attrs: &[],
2354 example_html: r#"<button class="moss-nav-island-sections" aria-expanded="false"><svg>…</svg></button>"#,
2355 example_markdown: "",
2356 status: Status::Emerging,
2357 since: "0",
2358 description: "Opens this page's section list. Ships `hidden` and is unhidden only once headings have been found, so a page with no headings shows no dead glyph.",
2359 },
2360 ComponentEntry {
2361 class: "moss-nav-island-menu",
2362 kind: "instance",
2363 parent: "moss-nav-island",
2364 data_attrs: &[DataAttr {
2365 name: "data-island-menu",
2366 values: &["levels", "sections"],
2367 default: "levels",
2368 description: "Which of the two menus this is: the folded ancestor levels, or the page's sections.",
2369 }],
2370 example_html: r#"<div class="moss-nav-island-menu" data-island-menu="sections">…</div>"#,
2371 example_markdown: "",
2372 status: Status::Emerging,
2373 since: "0",
2374 description: "Popover opened by `.moss-nav-island-more` or `.moss-nav-island-sections`. Every row reserves a leading gutter for the current-row rule, so the labels line up in one column whether or not a row is marked.",
2375 },
2376 ComponentEntry {
2377 class: "moss-breadcrumb-more",
2378 kind: "instance",
2379 parent: "",
2380 data_attrs: &[],
2381 example_html: r#"<button class="moss-breadcrumb-more" aria-expanded="false">…</button>"#,
2382 example_markdown: "",
2383 status: Status::Emerging,
2384 since: "0",
2385 description: "The masthead trail's counterpart to `.moss-nav-island-more`: stands in for the ancestor levels the trail folded, opens the levels menu on click, names them on hover via `data-tooltip`. Emitted (hidden) only when the trail has a middle to fold — three or more crumbs.",
2386 },
2387 ComponentEntry {
2388 class: "moss-breadcrumb-menu",
2389 kind: "instance",
2390 parent: "",
2391 data_attrs: &[],
2392 example_html: r#"<div class="moss-breadcrumb-menu" hidden>…</div>"#,
2393 example_markdown: "",
2394 status: Status::Emerging,
2395 since: "0",
2396 description: "Popover listing the masthead trail's folded ancestor levels, opened by `.moss-breadcrumb-more`. A sibling of `.nav-left` (which clips its own overflow), positioned against `.nav-content`. Same shape as `.moss-nav-island-menu`.",
2397 },
2398 ComponentEntry {
2399 class: "moss-nav-island-progress",
2400 kind: "instance",
2401 parent: "moss-nav-island-bar",
2402 data_attrs: &[],
2403 example_html: r#"<span class="moss-nav-island-progress"><span class="moss-nav-island-progress-fill"></span></span>"#,
2404 example_markdown: "",
2405 status: Status::Emerging,
2406 since: "0",
2407 description: "Reading-progress track along the island's own bottom edge — not a separate bar across the window. Currently measures document scroll.",
2408 },
2409 ComponentEntry {
2410 class: "moss-nav-island-progress-fill",
2411 kind: "instance",
2412 parent: "moss-nav-island-progress",
2413 data_attrs: &[],
2414 example_html: r#"<span class="moss-nav-island-progress-fill" style="width: 42%"></span>"#,
2415 example_markdown: "",
2416 status: Status::Emerging,
2417 since: "0",
2418 description: "The filled portion of the progress track. Its `width` is written inline by the site runtime; with JavaScript off it stays at 0 and the track reads as empty.",
2419 },
2420 ComponentEntry {
2421 class: "footer-default",
2422 kind: "chrome",
2423 parent: "",
2424 data_attrs: &[],
2425 example_html: r#"<p class="footer-default"><a href="/rss.xml" class="footer-link" data-external>RSS</a>…</p>"#,
2426 example_markdown: "",
2427 status: Status::Confirmed,
2428 since: "0",
2429 description: "The generated footer link row, emitted only when the site has no authored `footer.md`. Authoring a footer replaces it, so a rule targeting this stops applying the moment the site gains one.",
2430 },
2431 ComponentEntry {
2432 class: "footer-link",
2433 kind: "instance",
2434 parent: "footer-default",
2435 data_attrs: &[],
2436 example_html: r#"<a href="/rss.xml" class="footer-link" data-external>RSS</a>"#,
2437 example_markdown: "",
2438 status: Status::Confirmed,
2439 since: "0",
2440 description: "One link in the generated footer row (RSS and similar). Off-site ones also carry `data-external`.",
2441 },
2442 ComponentEntry {
2443 class: "moss-child-section-divider",
2444 kind: "instance",
2445 parent: "",
2446 data_attrs: &[],
2447 example_html: r#"<hr class="moss-child-section-divider" />"#,
2448 example_markdown: "",
2449 status: Status::Emerging,
2450 since: "0",
2451 description: "Divider rule between auto-generated child sections.",
2452 },
2453 ComponentEntry {
2454 class: "moss-unknown-shortcode",
2460 kind: "standalone",
2461 parent: "",
2462 data_attrs: &[DataAttr {
2463 name: "data-name",
2464 values: &[],
2465 default: "",
2466 description: "The unrecognised shortcode name, as written by the author.",
2467 }],
2468 example_html: r#"<div class="moss-unknown-shortcode" data-name="foo">
2469
2470<p>body parsed as markdown</p>
2471
2472</div>"#,
2473 example_markdown: ":::foo\nbody parsed as markdown\n:::",
2474 status: Status::Confirmed,
2475 since: "0",
2476 description: "Fallback wrapper emitted for any `:::name` fence whose name is not a registered shortcode. The body is still parsed as markdown and a build warning names the shortcode, so a misspelling degrades to a styled region rather than losing content.",
2477 },
2478 ComponentEntry {
2482 class: "moss-hl-keyword",
2483 kind: "instance",
2484 parent: "",
2485 data_attrs: &[],
2486 example_html: r#"<span class="moss-hl-keyword">if</span>"#,
2487 example_markdown: "",
2488 status: Status::Emerging,
2489 since: "0",
2490 description: "Syntax-highlight token: keyword.",
2491 },
2492 ComponentEntry {
2493 class: "moss-hl-string",
2494 kind: "instance",
2495 parent: "",
2496 data_attrs: &[],
2497 example_html: r#"<span class="moss-hl-string">"hi"</span>"#,
2498 example_markdown: "",
2499 status: Status::Emerging,
2500 since: "0",
2501 description: "Syntax-highlight token: string literal.",
2502 },
2503 ComponentEntry {
2504 class: "moss-hl-comment",
2505 kind: "instance",
2506 parent: "",
2507 data_attrs: &[],
2508 example_html: r#"<span class="moss-hl-comment">// note</span>"#,
2509 example_markdown: "",
2510 status: Status::Emerging,
2511 since: "0",
2512 description: "Syntax-highlight token: comment.",
2513 },
2514 ComponentEntry {
2515 class: "moss-hl-function",
2516 kind: "instance",
2517 parent: "",
2518 data_attrs: &[],
2519 example_html: r#"<span class="moss-hl-function">render</span>"#,
2520 example_markdown: "",
2521 status: Status::Emerging,
2522 since: "0",
2523 description: "Syntax-highlight token: function name.",
2524 },
2525 ComponentEntry {
2526 class: "moss-hl-type",
2527 kind: "instance",
2528 parent: "",
2529 data_attrs: &[],
2530 example_html: r#"<span class="moss-hl-type">String</span>"#,
2531 example_markdown: "",
2532 status: Status::Emerging,
2533 since: "0",
2534 description: "Syntax-highlight token: type name.",
2535 },
2536 ComponentEntry {
2537 class: "moss-hl-number",
2538 kind: "instance",
2539 parent: "",
2540 data_attrs: &[],
2541 example_html: r#"<span class="moss-hl-number">42</span>"#,
2542 example_markdown: "",
2543 status: Status::Emerging,
2544 since: "0",
2545 description: "Syntax-highlight token: numeric literal.",
2546 },
2547 ComponentEntry {
2548 class: "moss-hl-operator",
2549 kind: "instance",
2550 parent: "",
2551 data_attrs: &[],
2552 example_html: r#"<span class="moss-hl-operator">+</span>"#,
2553 example_markdown: "",
2554 status: Status::Emerging,
2555 since: "0",
2556 description: "Syntax-highlight token: operator.",
2557 },
2558 ComponentEntry {
2559 class: "moss-hl-builtin",
2560 kind: "instance",
2561 parent: "",
2562 data_attrs: &[],
2563 example_html: r#"<span class="moss-hl-builtin">print</span>"#,
2564 example_markdown: "",
2565 status: Status::Emerging,
2566 since: "0",
2567 description: "Syntax-highlight token: builtin identifier.",
2568 },
2569 ComponentEntry {
2570 class: "moss-hl-tag",
2571 kind: "instance",
2572 parent: "",
2573 data_attrs: &[],
2574 example_html: r#"<span class="moss-hl-tag">div</span>"#,
2575 example_markdown: "",
2576 status: Status::Emerging,
2577 since: "0",
2578 description: "Syntax-highlight token: markup tag name.",
2579 },
2580 ComponentEntry {
2581 class: "moss-hl-attr",
2582 kind: "instance",
2583 parent: "",
2584 data_attrs: &[],
2585 example_html: r#"<span class="moss-hl-attr">class</span>"#,
2586 example_markdown: "",
2587 status: Status::Emerging,
2588 since: "0",
2589 description: "Syntax-highlight token: attribute name.",
2590 },
2591 ComponentEntry {
2592 class: "moss-hl-meta",
2593 kind: "instance",
2594 parent: "",
2595 data_attrs: &[],
2596 example_html: r#"<span class="moss-hl-meta">@derive</span>"#,
2597 example_markdown: "",
2598 status: Status::Emerging,
2599 since: "0",
2600 description: "Syntax-highlight token: meta/annotation.",
2601 },
2602 ComponentEntry {
2603 class: "moss-hl-addition-bg",
2604 kind: "instance",
2605 parent: "",
2606 data_attrs: &[],
2607 example_html: r#"<span class="moss-hl-addition-bg">+ added line</span>"#,
2608 example_markdown: "",
2609 status: Status::Emerging,
2610 since: "0",
2611 description: "Syntax-highlight diff token: added-line background.",
2612 },
2613 ComponentEntry {
2614 class: "moss-hl-deletion",
2615 kind: "instance",
2616 parent: "",
2617 data_attrs: &[],
2618 example_html: r#"<span class="moss-hl-deletion">- removed line</span>"#,
2619 example_markdown: "",
2620 status: Status::Emerging,
2621 since: "0",
2622 description: "Syntax-highlight diff token: removed-line text.",
2623 },
2624 ComponentEntry {
2625 class: "moss-hl-deletion-bg",
2626 kind: "instance",
2627 parent: "",
2628 data_attrs: &[],
2629 example_html: r#"<span class="moss-hl-deletion-bg">- removed line</span>"#,
2630 example_markdown: "",
2631 status: Status::Emerging,
2632 since: "0",
2633 description: "Syntax-highlight diff token: removed-line background.",
2634 },
2635 ComponentEntry {
2636 class: "moss-recent",
2637 kind: "container",
2638 parent: "",
2639 data_attrs: &[],
2640 example_html: r#"<ul class="moss-recent">
2641 <li><a href="/posts/spring-notes/">Spring notes</a><div class="moss-recent__date">2026-04-12</div><div class="moss-recent__desc">A walk through the garden.</div></li>
2642</ul>"#,
2643 example_markdown: ":::recent {count=5 since=\"2026-01-01\"}\n:::\n",
2644 status: Status::Emerging,
2645 since: "0",
2646 description: "Auto-generated list of recent posts. Sorted newest-first; date and description slots are filled per child. No default CSS in the bundled theme — theme authors style it freely. IMPORTANT: emitted by the EMAIL/newsletter path only. On a web page, `:::recent` renders its fallback body as ordinary markdown and emits no list, because the per-page processor has no access to the build's aggregate document slice; a `:::recent` block with an empty body therefore produces nothing at all on a web page. To list posts on a page today, rely on the automatic child listing a folder home emits (`moss-cards`), and give any `:::recent` block a fallback body.",
2647 },
2648 ComponentEntry {
2649 class: "moss-recent__date",
2650 kind: "instance",
2651 parent: "moss-recent",
2652 data_attrs: &[],
2653 example_html: r#"<div class="moss-recent__date">2026-04-12</div>"#,
2654 example_markdown: "",
2655 status: Status::Emerging,
2656 since: "0",
2657 description: "Per-entry date slot inside `.moss-recent` (BEM child). Format is `YYYY-MM-DD`, derived from frontmatter `date`. Empty string when the post lacks a parseable date.",
2658 },
2659 ComponentEntry {
2660 class: "moss-recent__desc",
2661 kind: "instance",
2662 parent: "moss-recent",
2663 data_attrs: &[],
2664 example_html: r#"<div class="moss-recent__desc">A walk through the garden.</div>"#,
2665 example_markdown: "",
2666 status: Status::Emerging,
2667 since: "0",
2668 description: "Per-entry description slot inside `.moss-recent` (BEM child). Sourced from frontmatter `description`; empty when unset.",
2669 },
2670 ComponentEntry {
2676 class: "moss-ambient-video",
2677 kind: "standalone",
2678 parent: "",
2679 data_attrs: &[
2680 DataAttr {
2681 name: "data-paused",
2682 values: &[],
2683 default: "",
2684 description: "Boolean presence flag set by ambient-video.ts when the video is paused (user-initiated or reduced-motion guard). CSS uses `[data-paused]` to keep the toggle visible.",
2685 },
2686 ],
2687 example_html: r#"<div class="moss-ambient-video">
2688 <video data-loop src="clip.mp4" autoplay muted loop playsinline preload="metadata"></video>
2689 <button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>
2690</div>"#,
2691 example_markdown: "![[clip.mp4|loop]]",
2692 status: Status::Emerging,
2693 since: "1",
2694 description: "JS-injected wrapper around a `video[data-loop]` element. Provides the positioning context for `.moss-ambient-toggle` and the `[data-paused]` state hook. Not emitted by the Rust synthesizer — ambient-video.ts creates it at init.",
2695 },
2696 ComponentEntry {
2697 class: "moss-ambient-toggle",
2698 kind: "instance",
2699 parent: "moss-ambient-video",
2700 data_attrs: &[],
2701 example_html: r#"<button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>"#,
2702 example_markdown: "",
2703 status: Status::Emerging,
2704 since: "1",
2705 description: "Chrome-free pause/play toggle button for ambient loop videos. Injected by ambient-video.ts. Keyboard-focusable; `aria-label` toggles between \"Pause video\" and \"Play video\". Visible on hover/focus of `.moss-ambient-video` and always visible when `[data-paused]`. Satisfies WCAG 2.2.2 Level A (Pause, Stop, Hide).",
2706 },
2707 ComponentEntry {
2714 class: "moss-math",
2715 kind: "standalone",
2716 parent: "",
2717 data_attrs: &[
2718 DataAttr {
2719 name: "data-moss-math",
2720 values: &["inline", "display"],
2721 default: "inline",
2722 description: "Which delimiter produced the equation: `inline` for `$…$`, `display` for `$$…$$`. Carries the distinction to CSS and to the typesetter so neither has to re-derive it from context — a theme can select on it today to centre display math; moss ships no math stylesheet of its own yet, so both variants currently inherit plain `<code>` styling.",
2723 },
2724 ],
2725 example_html: r#"<code class="moss-math" data-moss-math="inline">$E = mc^2$</code>"#,
2726 example_markdown: "Energy $E = mc^2$.",
2727 status: Status::Emerging,
2728 since: "1",
2729 description: "A LaTeX equation. In P1 the element holds the author's own markdown source — `$` / `$$` delimiters included — HTML-escaped: an honest fallback that never shows a blank where an equation was written, and never deletes the delimiters of prose that merely looked like math. Requires `[site].math` (default on).",
2730 },
2731 ComponentEntry {
2732 class: "moss-math-scroll",
2733 kind: "container",
2734 parent: "",
2735 data_attrs: &[],
2736 example_html: r#"<div class="moss-math-scroll"><svg class="moss-math" data-moss-math="display">…</svg></div>"#,
2737 example_markdown: "$$E = mc^2$$",
2738 status: Status::Emerging,
2739 since: "1",
2740 description: "Horizontal-scroll container the build path wraps around typeset display math (`svg.moss-math[data-moss-math=\"display\"]`). On a narrow viewport a wide equation scrolls inside this box at its natural size rather than shrinking to unreadability or pushing the page into horizontal overflow. Emitted only by P2's typeset path; the P1 `<code>` fallback is never wrapped. A display SVG left unwrapped still cannot overflow the page — it falls back to scaling down via `max-width: 100%`.",
2741 },
2742 ComponentEntry {
2743 class: "moss-table-scroll",
2744 kind: "container",
2745 parent: "",
2746 data_attrs: &[],
2747 example_html: r#"<div class="moss-table-scroll" tabindex="0">
2748 <table>…</table>
2749</div>"#,
2750 example_markdown: "| Name | Subs |\n| --- | --- |\n| a | 1,200 |",
2751 status: Status::Emerging,
2752 since: "1",
2753 description: "Horizontal-scroll wrapper the renderer emits around every Markdown table and every CSV/TSV embed (`.moss-embed[data-type=\"table\"]`). Keeps the `<table>` semantically intact (unlike a `display:block` table, which breaks column layout and assistive-tech table semantics) while letting a wide table scroll inside its own box instead of pushing the page into horizontal overflow. `tabindex=\"0\"` makes an overflowing table keyboard-scrollable.",
2754 },
2755 ComponentEntry {
2756 class: "moss-col-right",
2757 kind: "instance",
2758 parent: "moss-table-scroll",
2759 data_attrs: &[],
2760 example_html: r#"<th class="moss-col-right">订阅数</th>
2761<td class="moss-col-right">1,457,776</td>"#,
2762 example_markdown: "| Subs |\n| --: |\n| 1,457,776 |",
2763 status: Status::Emerging,
2764 since: "1",
2765 description: "Right-aligned table cell (`<th>`/`<td>`). Applied to a whole column when the author right-aligned it in GFM (`|--:|`) or when the column auto-detects as numeric, so figures register on their trailing digits. Pairs with the table's `font-variant-numeric: tabular-nums`.",
2766 },
2767 ComponentEntry {
2768 class: "moss-col-center",
2769 kind: "instance",
2770 parent: "moss-table-scroll",
2771 data_attrs: &[],
2772 example_html: r#"<th class="moss-col-center">Status</th>
2773<td class="moss-col-center">✓</td>"#,
2774 example_markdown: "| Status |\n| :-: |\n| ✓ |",
2775 status: Status::Emerging,
2776 since: "1",
2777 description: "Center-aligned table cell (`<th>`/`<td>`). Applied to a whole column the author center-aligned in GFM (`|:-:|`).",
2778 },
2779 ComponentEntry {
2780 class: "moss-search",
2781 kind: "chrome",
2782 parent: "",
2783 data_attrs: &[],
2784 example_html: r#"<div class="moss-search" id="moss-search" hidden>
2785 <div class="moss-search__backdrop"></div>
2786 <div class="moss-search__panel" role="dialog" aria-modal="true" aria-label="Search">
2787 <div class="moss-search__field"><input class="moss-search__input" role="combobox"></div>
2788 <div class="moss-search__progress" hidden></div>
2789 <div class="moss-search__seam"></div>
2790 <div class="moss-search__body">
2791 <p class="moss-search__status" role="status" hidden></p>
2792 <ul class="moss-search__results" role="listbox">
2793 <li class="moss-search__row">
2794 <a class="moss-search__link" role="option" href="/posts/foo/">
2795 <span class="moss-search__title">Title</span>
2796 <span class="moss-search__excerpt">…a <mark>match</mark>…</span>
2797 </a>
2798 </li>
2799 </ul>
2800 </div>
2801 </div>
2802</div>"#,
2803 example_markdown: "",
2804 status: Status::Emerging,
2805 since: "1",
2806 description: "Site-search overlay. Not emitted by the build — the client runtime (`_moss/js/search.<hash>.js`, shipped only when the build wrote a Pagefind index) constructs this subtree lazily on the first open, so a reader who never searches downloads no index and materializes no DOM. Opened by the nav's `.nav-search-btn`, by `/`, or by ⌘K/Ctrl+K. BEM children carry the interior: `__backdrop` (translucent page-coloured scrim, not an opaque modal takeover), `__panel` (top-anchored at 18vh, fixed 18px radius at any height), `__field`/`__input`, `__progress` (1px accent hairline, delayed 200ms so fast queries never flash it), `__seam` (hairline inset by the corner radius), `__status` (idle / no-matches line, sharing one vertical slot with the results so the panel never jumps), `__results`/`__row`/`__link`/`__title`/`__excerpt`. Selection is a 2px `--moss-color-ui-accent` left border plus a ~4% accent tint — never a solid fill block. `<mark>` inside `__excerpt` is Pagefind's own term highlighting, restyled to colour emphasis rather than a highlighter box.",
2807 },
2808 ComponentEntry {
2809 class: "moss-footnotes",
2810 kind: "container",
2811 parent: "",
2812 data_attrs: &[],
2813 example_html: r##"<section class="moss-footnotes" role="doc-endnotes">
2814<ol>
2815<li id="fn-1"><p>The note. <a class="moss-footnote-backref" href="#fnref-1" role="doc-backlink" aria-label="Back to reference 1">↩︎</a></p>
2816</li>
2817</ol>
2818</section>"##,
2819 example_markdown: "Text[^1].\n\n[^1]: The note.",
2820 status: Status::Emerging,
2821 since: "1",
2822 description: "The document's endnote section, appended after the body by the renderer. Holds one `<li id=\"fn-N\">` per footnote in first-reference order, whatever depth the author wrote the definition at — a definition inside a blockquote or a list item is hoisted here too. Present only on pages that define at least one footnote. `role=\"doc-endnotes\"` (DPUB-ARIA) names the region for assistive tech.",
2823 },
2824 ComponentEntry {
2825 class: "moss-footnote-ref",
2826 kind: "instance",
2827 parent: "moss-footnotes",
2828 data_attrs: &[],
2829 example_html: r##"<sup class="moss-footnote-ref" id="fnref-1"><a href="#fn-1" role="doc-noteref">1</a></sup>"##,
2830 example_markdown: "Text[^1].\n\n[^1]: The note.",
2831 status: Status::Emerging,
2832 since: "1",
2833 description: "The in-body footnote marker: a superscript number linking down to its note. The number is first-reference order, not the author's label, so `[^method]` and `[^1]` both print as ordinals. A second marker for the same note takes id `fnref-N-2`, `fnref-N-3`, … so each has its own back-link.",
2834 },
2835 ComponentEntry {
2836 class: "moss-footnote-backref",
2837 kind: "instance",
2838 parent: "moss-footnotes",
2839 data_attrs: &[],
2840 example_html: r##"<a class="moss-footnote-backref" href="#fnref-1" role="doc-backlink" aria-label="Back to reference 1">↩︎</a>"##,
2841 example_markdown: "Text[^1].\n\n[^1]: The note.",
2842 status: Status::Emerging,
2843 since: "1",
2844 description: "The return arrow at the end of a note, linking back to the marker that sent the reader there. One per marker, so a note referenced twice ends with two arrows. A note nobody referenced has none. The arrow carries VARIATION SELECTOR-15 (`︎`) so mobile Chrome renders it as plain text rather than a coloured emoji.",
2845 },
2846 ComponentEntry {
2857 class: "container",
2858 kind: "chrome",
2859 parent: "",
2860 data_attrs: &[
2861 DataAttr {
2862 name: "data-share-cover",
2863 values: &[],
2864 default: "",
2865 description: "The page's own cover image, as a same-origin URL: its `:::hero` image, else its `cover:` frontmatter. Emitted on the `<article>` only, and only when the page has one of those — absent means the page genuinely has no cover, and never the auto-generated og card. The share-card runtime reads it instead of hunting for the cover in the markup; a theme can use `article.container[data-share-cover]` to tell a page that has a cover picture from one that does not.",
2866 },
2867 ],
2868 example_html: r#"<article class="container" data-share-cover="/img/cover.webp">…</article>"#,
2869 example_markdown: "",
2870 status: Status::Confirmed,
2871 since: "0",
2872 description: "The reading-width wrapper. It is the `<article>` on a page or post, and also the `<nav class=\"main-nav container\">` — one class, one measure, so the masthead lines up with the text under it. Site JS treats `article.container` as \"the current document\": immersive mode promotes its direct-child iframes, and the share card reads its `data-share-cover`.",
2873 },
2874 ComponentEntry {
2875 class: "nav-content",
2876 kind: "chrome",
2877 parent: "main-nav",
2878 data_attrs: &[],
2879 example_html: r#"<nav class="main-nav container"><div class="nav-content">…</div></nav>"#,
2880 example_markdown: "",
2881 status: Status::Confirmed,
2882 since: "0",
2883 description: "The row inside the nav bar that holds `.nav-left` and `.nav-right`. It is the box the responsive nav measures itself against: when the two groups no longer fit on one line, `nav-split.ts` sets `data-nav-split` on this element and the links move to a second row.",
2884 },
2885 ComponentEntry {
2886 class: "font-anchor",
2887 kind: "chrome",
2888 parent: "date-line",
2889 data_attrs: &[],
2890 example_html: r#"<div class="font-anchor"><button class="font-trigger size-std" aria-expanded="false"></button><div class="font-pill" id="fontPill">…</div></div>"#,
2891 example_markdown: "",
2892 status: Status::Confirmed,
2893 since: "0",
2894 description: "Positioning box for the reading-preferences control that sits at the end of an article's date line. Holds the trigger and the size pills; exists so the pills can be positioned against the trigger rather than the page.",
2895 },
2896 ComponentEntry {
2897 class: "font-trigger",
2898 kind: "instance",
2899 parent: "font-anchor",
2900 data_attrs: &[],
2901 example_html: r#"<button class="font-trigger size-std" aria-label="Reading preferences" aria-expanded="false" type="button"></button>"#,
2902 example_markdown: "",
2903 status: Status::Confirmed,
2904 since: "0",
2905 description: "The button that opens the reading-size pills. Its second class tracks the chosen size (`size-std` by default), so a theme can restyle the trigger per size without reading state from JS.",
2906 },
2907 ComponentEntry {
2908 class: "cover-thumb",
2909 kind: "instance",
2910 parent: "moss-card-cover",
2911 data_attrs: &[],
2912 example_html: r#"<div class="moss-card-cover"><video src="clip.mp4" muted loop playsinline preload="metadata"></video><img src="clip.thumb.jpg" alt="A clip" class="cover-thumb" /></div>"#,
2913 example_markdown: "",
2914 status: Status::Confirmed,
2915 since: "0",
2916 description: "The still frame stacked over a video cover. It is what the reader sees until they hover: `card-video.ts` fades this image out and starts the `<video>` underneath, and fades it back in on leave. Emitted only for video covers, inside `.moss-card-cover` or `.moss-collection-cover`.",
2917 },
2918 ComponentEntry {
2919 class: "media-item",
2920 kind: "instance",
2921 parent: "",
2922 data_attrs: &[
2923 DataAttr { name: "data-type", values: &["image", "video", "iframe"], default: "image", description: "Which lightbox surface opens for this item." },
2924 DataAttr { name: "data-src", values: &[], default: "", description: "Full-size source the lightbox loads." },
2925 DataAttr { name: "data-title", values: &[], default: "", description: "Caption shown under the lightbox." },
2926 DataAttr { name: "data-article", values: &[], default: "", description: "URL of the article the item came from, linked from the caption." },
2927 ],
2928 example_html: r#"<figure class="media-item" tabindex="0" data-type="image" data-src="/img/full.webp" data-title="Kyiv" data-article="/posts/kyiv/">…</figure>"#,
2929 example_markdown: "",
2930 status: Status::Confirmed,
2931 since: "0",
2932 description: "One tile on the media-collection page. `fullscreen.ts` collects these in document order to build the lightbox playlist, so their order on the page is the order the arrows step through.",
2933 },
2934 ComponentEntry {
2935 class: "lightbox-content",
2936 kind: "chrome",
2937 parent: "",
2938 data_attrs: &[],
2939 example_html: r#"<div class="lightbox-content"><img class="lightbox-image" hidden /><video class="lightbox-video" controls hidden></video><iframe class="lightbox-iframe" hidden></iframe></div>"#,
2940 example_markdown: "",
2941 status: Status::Confirmed,
2942 since: "0",
2943 description: "The stage of the media-collection lightbox. Holds all three players at once; `fullscreen.ts` unhides whichever one matches the opened item's `data-type` and leaves the others hidden.",
2944 },
2945 ComponentEntry {
2946 class: "lightbox-image",
2947 kind: "instance",
2948 parent: "lightbox-content",
2949 data_attrs: &[],
2950 example_html: r#"<img class="lightbox-image" src="" alt="" hidden />"#,
2951 example_markdown: "",
2952 status: Status::Confirmed,
2953 since: "0",
2954 description: "The lightbox's image player. Emitted empty and hidden; its `src` is filled in when an image item opens.",
2955 },
2956 ComponentEntry {
2957 class: "lightbox-video",
2958 kind: "instance",
2959 parent: "lightbox-content",
2960 data_attrs: &[],
2961 example_html: r#"<video class="lightbox-video" controls hidden></video>"#,
2962 example_markdown: "",
2963 status: Status::Confirmed,
2964 since: "0",
2965 description: "The lightbox's video player. Emitted empty and hidden; paused and cleared when the lightbox closes so audio never outlives the overlay.",
2966 },
2967 ComponentEntry {
2968 class: "lightbox-iframe",
2969 kind: "instance",
2970 parent: "lightbox-content",
2971 data_attrs: &[],
2972 example_html: r#"<iframe class="lightbox-iframe" hidden></iframe>"#,
2973 example_markdown: "",
2974 status: Status::Confirmed,
2975 since: "0",
2976 description: "The lightbox's embed surface, for media items that are an external player rather than a file.",
2977 },
2978 ComponentEntry {
2979 class: "lightbox-title",
2980 kind: "instance",
2981 parent: "",
2982 data_attrs: &[],
2983 example_html: r#"<p class="lightbox-title"></p>"#,
2984 example_markdown: "",
2985 status: Status::Confirmed,
2986 since: "0",
2987 description: "Caption line under the lightbox stage. Filled from the open item's `data-title`.",
2988 },
2989 ComponentEntry {
2990 class: "lightbox-article-link",
2991 kind: "instance",
2992 parent: "",
2993 data_attrs: &[],
2994 example_html: r#"<a class="lightbox-article-link" href="">View in article →</a>"#,
2995 example_markdown: "",
2996 status: Status::Confirmed,
2997 since: "0",
2998 description: "The way back from a media tile to the article it appeared in. Its `href` is filled from the open item's `data-article`, and it is hidden when the item has none.",
2999 },
3000 ComponentEntry {
3001 class: "lightbox-close",
3002 kind: "instance",
3003 parent: "",
3004 data_attrs: &[],
3005 example_html: r#"<button class="lightbox-close" aria-label="Close">×</button>"#,
3006 example_markdown: "",
3007 status: Status::Confirmed,
3008 since: "0",
3009 description: "Dismisses the media-collection lightbox. Escape does the same thing.",
3010 },
3011 ComponentEntry {
3012 class: "lightbox-next",
3013 kind: "instance",
3014 parent: "",
3015 data_attrs: &[],
3016 example_html: r#"<button class="lightbox-nav lightbox-next" aria-label="Next">›</button>"#,
3017 example_markdown: "",
3018 status: Status::Confirmed,
3019 since: "0",
3020 description: "Steps forward through the `.media-item` playlist, wrapping at the end. Carries `lightbox-nav` as well, which styles both arrows together.",
3021 },
3022 ComponentEntry {
3023 class: "lightbox-prev",
3024 kind: "instance",
3025 parent: "",
3026 data_attrs: &[],
3027 example_html: r#"<button class="lightbox-nav lightbox-prev" aria-label="Previous">‹</button>"#,
3028 example_markdown: "",
3029 status: Status::Confirmed,
3030 since: "0",
3031 description: "Steps backward through the `.media-item` playlist, wrapping at the start.",
3032 },
3033 ComponentEntry {
3034 class: "comments-toggle",
3035 kind: "instance",
3036 parent: "moss-comments",
3037 data_attrs: &[],
3038 example_html: r#"<summary class="comments-toggle"><svg class="comments-icon">…</svg><span>3 comments</span><svg class="comments-chevron">…</svg></summary>"#,
3039 example_markdown: "",
3040 status: Status::Confirmed,
3041 since: "0",
3042 description: "The `<summary>` that opens and closes the comment thread. Its `<span>` holds the count, which the client rewrites as comments arrive — so the span is a contract of its own, not decoration.",
3043 },
3044 ComponentEntry {
3045 class: "comment-list",
3046 kind: "container",
3047 parent: "moss-comments",
3048 data_attrs: &[],
3049 example_html: r#"<ol class="comment-list"><li class="comment-item" …>…</li></ol>"#,
3050 example_markdown: "",
3051 status: Status::Confirmed,
3052 since: "0",
3053 description: "The top-level comment thread, rendered server-side at build time and then hydrated in place (ADR-025). New comments are appended here by the client rather than replacing the list, so server-rendered and live comments share one shape.",
3054 },
3055 ComponentEntry {
3056 class: "comment-item",
3057 kind: "instance",
3058 parent: "comment-list",
3059 data_attrs: &[
3060 DataAttr { name: "data-comment-source", values: &["artalk"], default: "artalk", description: "Where the comment came from — moss's own server, or a syndicated platform." },
3061 DataAttr { name: "data-comment-id", values: &[], default: "", description: "Identifier within that source. Unique only per source, which is why nesting keys on the pair." },
3062 ],
3063 example_html: r#"<li class="comment-item" id="comment-artalk-12" data-comment-source="artalk" data-comment-id="12">…</li>"#,
3064 example_markdown: "",
3065 status: Status::Confirmed,
3066 since: "0",
3067 description: "One comment. Holds a `.comment-header`, a `.comment-body`, and — if it has replies — a nested `.comment-replies`. The `data-comment-*` pair is how the client matches a live comment to the one already on the page instead of rendering it twice.",
3068 },
3069 ComponentEntry {
3070 class: "comment-replies",
3071 kind: "container",
3072 parent: "comment-item",
3073 data_attrs: &[],
3074 example_html: r#"<ol class="comment-replies"><li class="comment-item" …>…</li></ol>"#,
3075 example_markdown: "",
3076 status: Status::Confirmed,
3077 since: "0",
3078 description: "Nested replies under a comment, same shape as `.comment-list`. Emitted only when a comment has replies; the client creates one on demand when the first reply arrives.",
3079 },
3080 ComponentEntry {
3081 class: "comment-reply-btn",
3082 kind: "instance",
3083 parent: "comment-item",
3084 data_attrs: &[
3085 DataAttr { name: "data-reply-id", values: &[], default: "", description: "The comment being replied to." },
3086 DataAttr { name: "data-reply-name", values: &[], default: "", description: "Display name of its author, used to prefill the form." },
3087 ],
3088 example_html: r#"<button type="button" class="comment-reply-btn" data-reply-id="12" data-reply-name="Yi">↩︎ Reply</button>"#,
3089 example_markdown: "",
3090 status: Status::Confirmed,
3091 since: "0",
3092 description: "Moves the comment form under this comment so the reply is written where it will appear. Emitted only for comments moss can reply to; a syndicated comment gets a link out to its own platform instead.",
3093 },
3094];
3095
3096const INTERNAL_CLASSES: &[&str] = &[
3102 "moss-apply",
3103 "moss-apply-form",
3104 "moss-apply-matters",
3105 "moss-apply-hp",
3106 "moss-apply-status",
3107 "moss-apply-helper",
3108];
3109
3110impl ComponentEntry {
3111 pub fn is_public(&self) -> bool {
3118 self.status != Status::Retired && !INTERNAL_CLASSES.contains(&self.class)
3119 }
3120}
3121
3122pub fn retired_class_names() -> impl Iterator<Item = &'static str> {
3128 COMPONENTS.iter()
3129 .filter(|e| e.status == Status::Retired)
3130 .map(|e| e.class)
3131}
3132
3133#[cfg(test)]
3134mod tests {
3135 use super::*;
3136
3137 #[test]
3143 fn every_internal_class_has_a_components_entry() {
3144 let component_classes: std::collections::HashSet<&'static str> =
3145 COMPONENTS.iter().map(|e| e.class).collect();
3146 for &internal in INTERNAL_CLASSES {
3147 assert!(
3148 component_classes.contains(internal),
3149 "INTERNAL_CLASSES entry '{}' has no matching entry in COMPONENTS — \
3150 add a ComponentEntry for it or remove it from INTERNAL_CLASSES",
3151 internal
3152 );
3153 }
3154 }
3155}