Skip to main content

moss_core/contract/
components.rs

1//! moss component contract — Source 2 of the federated contract.
2//!
3//! Single source of truth for every `moss-*` class moss currently emits.
4//! Each entry declares: the class name, its kind (container/instance/standalone/chrome),
5//! accepted `data-*` attributes with value spaces, example HTML, example markdown.
6//!
7//! ## Adding a new emitter class
8//!
9//! 1. Emit the class from your renderer module (`build/markdown/*`, `build/components/*`).
10//! 2. Add a `ComponentEntry` to [`COMPONENTS`] here.
11//! 3. Run `cargo test --test components_sync_test` from src-tauri/ — the
12//!    scanner test will fail if you forget.
13//! 4. Run `cargo run --bin generate-contract-docs --features dev-tools` to
14//!    refresh `docs/contract/reference.md`.
15//!
16//! ## Why a const table, not a derive macro?
17//!
18//! Mirrors the BUILTIN_FIELDS precedent in `schema_fields.rs`. The synchronization
19//! is enforced by a sync test (`emitter_classes_match_components_table`) that
20//! scans emitter Rust source for `class="moss-..."` literals. This is a
21//! best-effort scanner (won't catch classes assembled via `format!()`), not a
22//! type-checked guarantee like BUILTIN_FIELDS' compile-time mirror. The
23//! limitation is documented in the spec § Source 2.
24
25/// Status of a component entry.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Status {
28    /// In active use; theme authors can rely on it.
29    Confirmed,
30    /// Emerging convention; may evolve.
31    Emerging,
32    /// Scheduled for removal; theme authors should migrate.
33    Retired,
34}
35
36/// A declared `data-*` attribute on a component.
37pub struct DataAttr {
38    /// Attribute name including `data-` prefix (e.g. `"data-layout"`).
39    pub name: &'static str,
40    /// Allowed values (e.g. `&["grid", "list", "minimal"]`). Empty means free-form.
41    pub values: &'static [&'static str],
42    /// Default value (first in `values`, or `""` for free-form).
43    pub default: &'static str,
44    /// Short description shown in reference.md.
45    pub description: &'static str,
46}
47
48/// A single component contract entry.
49pub struct ComponentEntry {
50    /// Class name without leading `.` (e.g. `"moss-cards"`).
51    pub class: &'static str,
52    /// Container / Instance / Standalone / Chrome.
53    pub kind: &'static str,
54    /// For Instance kinds, the parent container's class (or `""`).
55    pub parent: &'static str,
56    /// Declared `data-*` attributes on the element with this class.
57    pub data_attrs: &'static [DataAttr],
58    /// Example HTML snippet showing the class in context. Multi-line allowed.
59    pub example_html: &'static str,
60    /// Example markdown that produces this HTML. Empty for HTML-only chrome.
61    pub example_markdown: &'static str,
62    /// Status: confirmed / emerging / retired.
63    pub status: Status,
64    /// Contract version this entry was introduced in.
65    pub since: &'static str,
66    /// Optional human-readable description.
67    pub description: &'static str,
68}
69
70/// The full contract surface — every `moss-*` class moss currently emits.
71///
72/// Phase 0b seeds this with the CURRENT emitted vocabulary (not the
73/// v1-collapsed shape). Phase 1c rewrites to the collapsed form.
74pub const COMPONENTS: &[ComponentEntry] = &[
75    ComponentEntry {
76        class: "moss-cards",
77        kind: "container",
78        parent: "",
79        data_attrs: &[
80            DataAttr {
81                name: "data-layout",
82                values: &["grid", "list", "minimal"],
83                default: "grid",
84                description: "Card layout density. Grid: 2-3 cols with covers. List: single column with side covers. Minimal: text-only with year groupings.",
85            },
86            DataAttr {
87                name: "data-density",
88                values: &["default", "compact"],
89                default: "default",
90                description: "Vertical spacing density.",
91            },
92            DataAttr {
93                name: "data-list-axis",
94                values: &["date", "weight", "title"],
95                default: "title",
96                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).",
97            },
98            DataAttr {
99                name: "data-list-has-covers",
100                values: &[""],
101                default: "",
102                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.",
103            },
104        ],
105        example_html: r#"<div class="moss-cards-container">
106  <div class="moss-cards" data-layout="grid" data-list-axis="date" data-list-has-covers>
107    <a class="moss-card" href="...">...</a>
108    <a class="moss-card" href="...">...</a>
109  </div>
110</div>"#,
111        example_markdown: "",
112        status: Status::Confirmed,
113        since: "1",
114        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.",
115    },
116    ComponentEntry {
117        class: "moss-cards-container",
118        kind: "container",
119        parent: "",
120        data_attrs: &[],
121        example_html: r#"<div class="moss-cards-container">
122  <div class="moss-cards" data-layout="grid">...</div>
123</div>"#,
124        example_markdown: "",
125        status: Status::Confirmed,
126        since: "1",
127        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.",
128    },
129    ComponentEntry {
130        class: "moss-summary-layout",
131        kind: "container",
132        parent: "moss-cards",
133        data_attrs: &[],
134        example_html: r#"<div class="moss-cards" data-layout="list">...</div>"#,
135        example_markdown: "",
136        status: Status::Retired,
137        since: "1",
138        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.",
139    },
140    // -------------------------------------------------------------------
141    // Cards family — current emitted vocabulary (pre-Phase 1c collapsing).
142    // Three parallel layouts: grid, list, minimal. Each has its own
143    // container + instance + sub-classes.
144    // -------------------------------------------------------------------
145    ComponentEntry {
146        class: "moss-cards-grid",
147        kind: "container",
148        parent: "",
149        data_attrs: &[],
150        example_html: r#"<div class="moss-cards-grid">
151  <a class="moss-card-grid" href="...">...</a>
152</div>"#,
153        example_markdown: "",
154        status: Status::Retired,
155        since: "0",
156        description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=grid]`.",
157    },
158    ComponentEntry {
159        class: "moss-cards-list",
160        kind: "container",
161        parent: "",
162        data_attrs: &[],
163        example_html: r#"<div class="moss-cards-list">
164  <a class="moss-card-list" href="...">...</a>
165</div>"#,
166        example_markdown: "",
167        status: Status::Retired,
168        since: "0",
169        description: "Retired in Phase 1c — collapsed into `.moss-cards[data-layout=list]`.",
170    },
171    ComponentEntry {
172        class: "moss-cards-minimal-year-group",
173        kind: "container",
174        parent: "",
175        data_attrs: &[],
176        example_html: r#"<section class="moss-cards-minimal-year-group">
177  <h3>2024</h3>
178  <div class="moss-card-minimal">...</div>
179</section>"#,
180        example_markdown: "",
181        status: Status::Confirmed,
182        since: "0",
183        description: "Year-grouped section in minimal card layout (e.g. blog index). Modifier `--summary` collapses past years.",
184    },
185    ComponentEntry {
186        class: "moss-cards-minimal-year-group--summary",
187        kind: "container",
188        parent: "moss-cards-minimal-year-group",
189        data_attrs: &[],
190        example_html: r#"<section class="moss-cards-minimal-year-group moss-cards-minimal-year-group--summary">...</section>"#,
191        example_markdown: "",
192        status: Status::Confirmed,
193        since: "0",
194        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).",
195    },
196    ComponentEntry {
197        class: "moss-card",
198        kind: "instance",
199        parent: "moss-cards",
200        data_attrs: &[
201            DataAttr {
202                name: "data-linkblog",
203                values: &[],
204                default: "",
205                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>`).",
206            },
207        ],
208        example_html: r#"<a class="moss-card" href="...">...</a>"#,
209        example_markdown: "",
210        status: Status::Confirmed,
211        since: "1",
212        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]`).",
213    },
214    ComponentEntry {
215        class: "moss-card-cover",
216        kind: "instance",
217        parent: "moss-card",
218        data_attrs: &[],
219        example_html: r#"<div class="moss-card-cover"><img src="..." /></div>"#,
220        example_markdown: "",
221        status: Status::Confirmed,
222        since: "1",
223        description: "Cover media slot inside `.moss-card`. Gets `.moss-card-no-cover` modifier when no image is present.",
224    },
225    ComponentEntry {
226        class: "moss-card-no-cover",
227        kind: "instance",
228        parent: "moss-card",
229        data_attrs: &[],
230        example_html: r#"<div class="moss-card-cover moss-card-no-cover"></div>"#,
231        example_markdown: "",
232        status: Status::Confirmed,
233        since: "1",
234        description: "Modifier applied to `.moss-card-cover` when no cover media is available.",
235    },
236    ComponentEntry {
237        class: "moss-card-content",
238        kind: "instance",
239        parent: "moss-card",
240        data_attrs: &[],
241        example_html: r#"<div class="moss-card-content">...</div>"#,
242        example_markdown: "",
243        status: Status::Confirmed,
244        since: "1",
245        description: "Text content slot inside a grid-layout `.moss-card` (kicker + title + meta).",
246    },
247    ComponentEntry {
248        class: "moss-card-row",
249        kind: "instance",
250        parent: "moss-card",
251        data_attrs: &[],
252        example_html: r#"<div class="moss-card-row">...</div>"#,
253        example_markdown: "",
254        status: Status::Confirmed,
255        since: "1",
256        description: "Row wrapper inside a list-layout `.moss-card` holding body + cover side-by-side.",
257    },
258    ComponentEntry {
259        class: "moss-card-body",
260        kind: "instance",
261        parent: "moss-card",
262        data_attrs: &[],
263        example_html: r#"<div class="moss-card-body">...</div>"#,
264        example_markdown: "",
265        status: Status::Confirmed,
266        since: "1",
267        description: "Text body slot of a list-layout `.moss-card`.",
268    },
269    ComponentEntry {
270        class: "moss-card-head",
271        kind: "instance",
272        parent: "moss-card",
273        data_attrs: &[],
274        example_html: r#"<div class="moss-card-head">...</div>"#,
275        example_markdown: "",
276        status: Status::Confirmed,
277        since: "1",
278        description: "Header row of a `.moss-card-body` (title + kicker + meta).",
279    },
280    ComponentEntry {
281        class: "moss-card-title",
282        kind: "instance",
283        parent: "moss-card",
284        data_attrs: &[],
285        example_html: r#"<h3 class="moss-card-title">Page title</h3>"#,
286        example_markdown: "",
287        status: Status::Confirmed,
288        since: "1",
289        description: "Title inside `.moss-card`.",
290    },
291    ComponentEntry {
292        class: "moss-card-meta",
293        kind: "instance",
294        parent: "moss-card",
295        data_attrs: &[],
296        example_html: r#"<div class="moss-card-meta">2024-01-15</div>"#,
297        example_markdown: "",
298        status: Status::Confirmed,
299        since: "1",
300        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/design-system/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.",
301    },
302    ComponentEntry {
303        class: "moss-card-kicker",
304        kind: "instance",
305        parent: "moss-card",
306        data_attrs: &[],
307        example_html: r#"<span class="moss-card-kicker">Category</span>"#,
308        example_markdown: "",
309        status: Status::Confirmed,
310        since: "1",
311        description: "Eyebrow / overline above the title inside `.moss-card`.",
312    },
313    ComponentEntry {
314        class: "moss-card-permalink",
315        kind: "instance",
316        parent: "moss-card-kicker",
317        data_attrs: &[],
318        example_html: r#"<a class="moss-card-permalink" href="/posts/foo/" title="Permalink to 'Title'">★</a>"#,
319        example_markdown: "",
320        status: Status::Emerging,
321        since: "1",
322        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`.",
323    },
324    ComponentEntry {
325        class: "moss-card-title-link",
326        kind: "instance",
327        parent: "moss-card-head",
328        data_attrs: &[],
329        example_html: r#"<a class="moss-card-title-link" href="https://outlet.example/article"><h3 class="moss-card-title">Article Title</h3></a>"#,
330        example_markdown: "",
331        status: Status::Emerging,
332        since: "1",
333        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`).",
334    },
335    ComponentEntry {
336        class: "moss-card-cover-link",
337        kind: "instance",
338        parent: "moss-card-row",
339        data_attrs: &[],
340        example_html: r#"<a class="moss-card-cover-link" href="https://outlet.example/article"><div class="moss-card-cover">...</div></a>"#,
341        example_markdown: "",
342        status: Status::Emerging,
343        since: "1",
344        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.",
345    },
346    ComponentEntry {
347        class: "moss-card-description-link",
348        kind: "instance",
349        parent: "moss-card-body",
350        data_attrs: &[],
351        example_html: r#"<a class="moss-card-description-link" href="https://outlet.example/article"><p class="moss-card-description">…</p></a>"#,
352        example_markdown: "",
353        status: Status::Emerging,
354        since: "1",
355        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.",
356    },
357    ComponentEntry {
358        class: "moss-card-description",
359        kind: "instance",
360        parent: "moss-card",
361        data_attrs: &[],
362        example_html: r#"<p class="moss-card-description">Excerpt...</p>"#,
363        example_markdown: "",
364        status: Status::Confirmed,
365        since: "1",
366        description: "Excerpt / description paragraph inside a `.moss-card` — below the title in both grid- and list-layout cards.",
367    },
368    ComponentEntry {
369        class: "moss-card-count",
370        kind: "instance",
371        parent: "moss-card",
372        data_attrs: &[],
373        example_html: r#"<div class="moss-card-count">4 articles</div>"#,
374        example_markdown: "",
375        status: Status::Confirmed,
376        since: "1",
377        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.",
378    },
379    ComponentEntry {
380        class: "moss-embed-more",
381        kind: "instance",
382        parent: "moss-cards-container",
383        data_attrs: &[],
384        example_html: r#"<p class="moss-embed-more"><a href="/news/">More →</a></p>"#,
385        example_markdown: "",
386        status: Status::Confirmed,
387        since: "1",
388        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/architecture/ui-design/spacing.md).",
389    },
390    ComponentEntry {
391        class: "moss-card-grid",
392        kind: "instance",
393        parent: "moss-cards-grid",
394        data_attrs: &[],
395        example_html: r#"<a class="moss-card-grid" href="...">...</a>"#,
396        example_markdown: "",
397        status: Status::Retired,
398        since: "0",
399        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=grid]`).",
400    },
401    ComponentEntry {
402        class: "moss-card-grid-cover",
403        kind: "instance",
404        parent: "moss-card-grid",
405        data_attrs: &[],
406        example_html: r#"<div class="moss-card-grid-cover"><img src="..." /></div>"#,
407        example_markdown: "",
408        status: Status::Retired,
409        since: "0",
410        description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
411    },
412    ComponentEntry {
413        class: "moss-card-grid-no-cover",
414        kind: "instance",
415        parent: "moss-card-grid",
416        data_attrs: &[],
417        example_html: r#"<div class="moss-card-grid-cover moss-card-grid-no-cover"></div>"#,
418        example_markdown: "",
419        status: Status::Retired,
420        since: "0",
421        description: "Retired in Phase 1c — collapsed into `.moss-card-no-cover`.",
422    },
423    ComponentEntry {
424        class: "moss-card-grid-content",
425        kind: "instance",
426        parent: "moss-card-grid",
427        data_attrs: &[],
428        example_html: r#"<div class="moss-card-grid-content">...</div>"#,
429        example_markdown: "",
430        status: Status::Retired,
431        since: "0",
432        description: "Retired in Phase 1c — collapsed into `.moss-card-content`.",
433    },
434    ComponentEntry {
435        class: "moss-card-grid-kicker",
436        kind: "instance",
437        parent: "moss-card-grid",
438        data_attrs: &[],
439        example_html: r#"<span class="moss-card-grid-kicker">Category</span>"#,
440        example_markdown: "",
441        status: Status::Retired,
442        since: "0",
443        description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
444    },
445    ComponentEntry {
446        class: "moss-card-grid-title",
447        kind: "instance",
448        parent: "moss-card-grid",
449        data_attrs: &[],
450        example_html: r#"<h3 class="moss-card-grid-title">Page title</h3>"#,
451        example_markdown: "",
452        status: Status::Retired,
453        since: "0",
454        description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
455    },
456    ComponentEntry {
457        class: "moss-card-grid-meta",
458        kind: "instance",
459        parent: "moss-card-grid",
460        data_attrs: &[],
461        example_html: r#"<div class="moss-card-grid-meta">2024-01-15</div>"#,
462        example_markdown: "",
463        status: Status::Retired,
464        since: "0",
465        description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
466    },
467    ComponentEntry {
468        class: "moss-card-list",
469        kind: "instance",
470        parent: "moss-cards-list",
471        data_attrs: &[],
472        example_html: r#"<a class="moss-card-list" href="...">...</a>"#,
473        example_markdown: "",
474        status: Status::Retired,
475        since: "0",
476        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=list]`).",
477    },
478    ComponentEntry {
479        class: "moss-card-list-row",
480        kind: "instance",
481        parent: "moss-card-list",
482        data_attrs: &[],
483        example_html: r#"<div class="moss-card-list-row">...</div>"#,
484        example_markdown: "",
485        status: Status::Retired,
486        since: "0",
487        description: "Retired in Phase 1c — collapsed into `.moss-card-row`.",
488    },
489    ComponentEntry {
490        class: "moss-card-list-cover",
491        kind: "instance",
492        parent: "moss-card-list",
493        data_attrs: &[],
494        example_html: r#"<div class="moss-card-list-cover"><img src="..." /></div>"#,
495        example_markdown: "",
496        status: Status::Retired,
497        since: "0",
498        description: "Retired in Phase 1c — collapsed into `.moss-card-cover`.",
499    },
500    ComponentEntry {
501        class: "moss-card-list-body",
502        kind: "instance",
503        parent: "moss-card-list",
504        data_attrs: &[],
505        example_html: r#"<div class="moss-card-list-body">...</div>"#,
506        example_markdown: "",
507        status: Status::Retired,
508        since: "0",
509        description: "Retired in Phase 1c — collapsed into `.moss-card-body`.",
510    },
511    ComponentEntry {
512        class: "moss-card-list-head",
513        kind: "instance",
514        parent: "moss-card-list",
515        data_attrs: &[],
516        example_html: r#"<div class="moss-card-list-head">...</div>"#,
517        example_markdown: "",
518        status: Status::Retired,
519        since: "0",
520        description: "Retired in Phase 1c — collapsed into `.moss-card-head`.",
521    },
522    ComponentEntry {
523        class: "moss-card-list-kicker",
524        kind: "instance",
525        parent: "moss-card-list",
526        data_attrs: &[],
527        example_html: r#"<span class="moss-card-list-kicker">Category</span>"#,
528        example_markdown: "",
529        status: Status::Retired,
530        since: "0",
531        description: "Retired in Phase 1c — collapsed into `.moss-card-kicker`.",
532    },
533    ComponentEntry {
534        class: "moss-card-list-title",
535        kind: "instance",
536        parent: "moss-card-list",
537        data_attrs: &[],
538        example_html: r#"<h3 class="moss-card-list-title">Page title</h3>"#,
539        example_markdown: "",
540        status: Status::Retired,
541        since: "0",
542        description: "Retired in Phase 1c — collapsed into `.moss-card-title`.",
543    },
544    ComponentEntry {
545        class: "moss-card-list-meta",
546        kind: "instance",
547        parent: "moss-card-list",
548        data_attrs: &[],
549        example_html: r#"<div class="moss-card-list-meta">2024-01-15</div>"#,
550        example_markdown: "",
551        status: Status::Retired,
552        since: "0",
553        description: "Retired in Phase 1c — collapsed into `.moss-card-meta`.",
554    },
555    ComponentEntry {
556        class: "moss-card-list-description",
557        kind: "instance",
558        parent: "moss-card-list",
559        data_attrs: &[],
560        example_html: r#"<p class="moss-card-list-description">Excerpt...</p>"#,
561        example_markdown: "",
562        status: Status::Retired,
563        since: "0",
564        description: "Retired in Phase 1c — collapsed into `.moss-card-description`.",
565    },
566    ComponentEntry {
567        class: "moss-card-minimal",
568        kind: "instance",
569        parent: "moss-cards-minimal-year-group",
570        data_attrs: &[],
571        example_html: r#"<div class="moss-card-minimal">
572  <a class="moss-prefix-link" href="...">...</a>
573</div>"#,
574        example_markdown: "",
575        status: Status::Retired,
576        since: "0",
577        description: "Retired in Phase 1c — collapsed into `.moss-card` (with parent `.moss-cards[data-layout=minimal]`).",
578    },
579    ComponentEntry {
580        class: "moss-folder-item",
581        kind: "instance",
582        parent: "moss-cards-minimal-year-group",
583        data_attrs: &[],
584        example_html: r#"<div class="moss-card-minimal moss-folder-item">
585  <a class="moss-prefix-link moss-folder-link" href="...">...</a>
586  <p class="moss-folder-description">...</p>
587</div>"#,
588        example_markdown: "",
589        status: Status::Confirmed,
590        since: "0",
591        description: "Modifier on `.moss-card-minimal` for folder-type entries in minimal listings.",
592    },
593    ComponentEntry {
594        class: "moss-folder-title",
595        kind: "instance",
596        parent: "moss-folder-item",
597        data_attrs: &[],
598        example_html: r#"<span class="moss-folder-title">Folder name</span>"#,
599        example_markdown: "",
600        status: Status::Confirmed,
601        since: "0",
602        description: "Title text of a folder entry in minimal listings.",
603    },
604    ComponentEntry {
605        class: "moss-folder-description",
606        kind: "instance",
607        parent: "moss-folder-item",
608        data_attrs: &[],
609        example_html: r#"<p class="moss-folder-description">Description...</p>"#,
610        example_markdown: "",
611        status: Status::Confirmed,
612        since: "0",
613        description: "Description paragraph of a folder entry in minimal listings.",
614    },
615    ComponentEntry {
616        class: "moss-folder-link",
617        kind: "instance",
618        parent: "moss-folder-item",
619        data_attrs: &[],
620        example_html: r#"<a class="moss-prefix-link moss-folder-link" href="...">...</a>"#,
621        example_markdown: "",
622        status: Status::Confirmed,
623        since: "0",
624        description: "Modifier on `.moss-prefix-link` for folder-type links in minimal listings.",
625    },
626    // -------------------------------------------------------------------
627    // Prefix-link primitive — used by minimal cards and other listings.
628    // -------------------------------------------------------------------
629    ComponentEntry {
630        class: "moss-prefix-link",
631        kind: "instance",
632        parent: "moss-card-minimal",
633        data_attrs: &[],
634        example_html: r#"<a class="moss-prefix-link" href="...">
635  <span class="moss-prefix-link-prefix">2024-01-15</span>
636  <span class="moss-prefix-link-title">Page title</span>
637</a>"#,
638        example_markdown: "",
639        status: Status::Emerging,
640        since: "0",
641        description: "Link with a prefix span (date or icon) and a title span. Used inside minimal cards.",
642    },
643    ComponentEntry {
644        class: "moss-prefix-link-prefix",
645        kind: "instance",
646        parent: "moss-prefix-link",
647        data_attrs: &[],
648        example_html: r#"<span class="moss-prefix-link-prefix">2024-01-15</span>"#,
649        example_markdown: "",
650        status: Status::Emerging,
651        since: "0",
652        description: "Prefix slot of a prefix-link (typically a date).",
653    },
654    ComponentEntry {
655        class: "moss-prefix-link-title",
656        kind: "instance",
657        parent: "moss-prefix-link",
658        data_attrs: &[],
659        example_html: r#"<span class="moss-prefix-link-title">Page title</span>"#,
660        example_markdown: "",
661        status: Status::Emerging,
662        since: "0",
663        description: "Title slot of a prefix-link.",
664    },
665    ComponentEntry {
666        class: "moss-prefix-link-suffix",
667        kind: "instance",
668        parent: "moss-prefix-link",
669        data_attrs: &[],
670        example_html: r#"<span class="moss-prefix-link-suffix">→</span>"#,
671        example_markdown: "",
672        status: Status::Emerging,
673        since: "0",
674        description: "Optional trailing slot of a prefix-link.",
675    },
676    // -------------------------------------------------------------------
677    // Callouts — Obsidian-style admonitions. Type variant goes on the
678    // container as `.callout-<type>`. Phase 1c may collapse into
679    // `.moss-callout[data-type]`.
680    // -------------------------------------------------------------------
681    ComponentEntry {
682        class: "moss-callout",
683        kind: "standalone",
684        parent: "",
685        data_attrs: &[],
686        example_html: r#"<div class="moss-callout callout" data-type="note">
687  <div class="callout-title">Note</div>
688  <div class="callout-content">Body...</div>
689</div>"#,
690        example_markdown: "> [!note]\n> Body...",
691        status: Status::Confirmed,
692        since: "0",
693        description: "Obsidian-style callout. The Obsidian-compat `.callout` class is co-emitted; type lives on `data-type` (v1).",
694    },
695    ComponentEntry {
696        class: "callout",
697        kind: "standalone",
698        parent: "",
699        data_attrs: &[
700            DataAttr {
701                name: "data-type",
702                values: &["note", "info", "tip", "warning", "pending"],
703                default: "note",
704                description: "v1 callout type. Theme authors target `.callout[data-type=...]` to style by variant.",
705            },
706        ],
707        example_html: r#"<div class="moss-callout callout" data-type="note">...</div>"#,
708        example_markdown: "",
709        status: Status::Confirmed,
710        since: "0",
711        description: "Obsidian-compat class co-emitted on every callout for theme parity. Type lives on `data-type` (v1).",
712    },
713    ComponentEntry {
714        class: "callout-title",
715        kind: "instance",
716        parent: "moss-callout",
717        data_attrs: &[],
718        example_html: r#"<div class="callout-title">Note</div>"#,
719        example_markdown: "",
720        status: Status::Confirmed,
721        since: "0",
722        description: "Title row of a callout.",
723    },
724    ComponentEntry {
725        class: "callout-content",
726        kind: "instance",
727        parent: "moss-callout",
728        data_attrs: &[],
729        example_html: r#"<div class="callout-content">Body...</div>"#,
730        example_markdown: "",
731        status: Status::Confirmed,
732        since: "0",
733        description: "Body container of a callout.",
734    },
735    ComponentEntry {
736        class: "callout-note",
737        kind: "instance",
738        parent: "moss-callout",
739        data_attrs: &[],
740        example_html: r#"<div class="moss-callout callout callout-note">...</div>"#,
741        example_markdown: "> [!note]\n> Body",
742        status: Status::Retired,
743        since: "0",
744        description: "Retired in Phase 1c — type lives on `.callout[data-type=note]`.",
745    },
746    ComponentEntry {
747        class: "callout-info",
748        kind: "instance",
749        parent: "moss-callout",
750        data_attrs: &[],
751        example_html: r#"<div class="moss-callout callout callout-info">...</div>"#,
752        example_markdown: "> [!info]\n> Body",
753        status: Status::Retired,
754        since: "0",
755        description: "Retired in Phase 1c — type lives on `.callout[data-type=info]`.",
756    },
757    ComponentEntry {
758        class: "callout-tip",
759        kind: "instance",
760        parent: "moss-callout",
761        data_attrs: &[],
762        example_html: r#"<div class="moss-callout callout callout-tip">...</div>"#,
763        example_markdown: "> [!tip]\n> Body",
764        status: Status::Retired,
765        since: "0",
766        description: "Retired in Phase 1c — type lives on `.callout[data-type=tip]`.",
767    },
768    ComponentEntry {
769        class: "callout-warning",
770        kind: "instance",
771        parent: "moss-callout",
772        data_attrs: &[],
773        example_html: r#"<div class="moss-callout callout callout-warning">...</div>"#,
774        example_markdown: "> [!warning]\n> Body",
775        status: Status::Retired,
776        since: "0",
777        description: "Retired in Phase 1c — type lives on `.callout[data-type=warning]`.",
778    },
779    ComponentEntry {
780        class: "callout-pending",
781        kind: "instance",
782        parent: "moss-callout",
783        data_attrs: &[],
784        example_html: r#"<div class="moss-callout callout callout-pending">...</div>"#,
785        example_markdown: "> [!pending]\n> Body",
786        status: Status::Retired,
787        since: "0",
788        description: "Retired in Phase 1c — type lives on `.callout[data-type=pending]`.",
789    },
790    // -------------------------------------------------------------------
791    // Embeds — `![[file.ext]]` shortcode renderers (audio, video, pdf,
792    // notebook, table, 3d, iframe).
793    // -------------------------------------------------------------------
794    ComponentEntry {
795        class: "moss-embed",
796        kind: "standalone",
797        parent: "",
798        data_attrs: &[
799            DataAttr {
800                name: "data-type",
801                values: &["audio", "video", "pdf", "notebook", "table", "iframe", "3d"],
802                default: "",
803                description: "v1 embed kind. Set on the embed element. Theme authors target `.moss-embed[data-type=...]`.",
804            },
805            DataAttr {
806                name: "data-loop",
807                values: &[],
808                default: "",
809                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.",
810            },
811            DataAttr {
812                name: "data-width",
813                values: &["body", "wide", "page", "screen"],
814                default: "body",
815                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
816            },
817            DataAttr {
818                name: "data-provider",
819                values: &["youtube", "vimeo", "codepen"],
820                default: "",
821                description: "Identifies the embed provider for external URL embeds. Absent for generic iframes and local HTML embeds.",
822            },
823        ],
824        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>"#,
825        example_markdown: "![[clip.mp4|loop]]",
826        status: Status::Confirmed,
827        since: "0",
828        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.",
829    },
830    ComponentEntry {
831        class: "moss-embed-audio",
832        kind: "instance",
833        parent: "moss-embed",
834        data_attrs: &[],
835        example_html: r#"<div class="moss-embed moss-embed-audio"><audio controls src="..."></audio></div>"#,
836        example_markdown: "![[track.mp3]]",
837        status: Status::Retired,
838        since: "0",
839        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=audio]`.",
840    },
841    ComponentEntry {
842        class: "moss-embed-video",
843        kind: "instance",
844        parent: "moss-embed",
845        data_attrs: &[],
846        example_html: r#"<div class="moss-embed moss-embed-video"><video controls src="..."></video></div>"#,
847        example_markdown: "![[clip.mp4]]",
848        status: Status::Retired,
849        since: "0",
850        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=video]`.",
851    },
852    ComponentEntry {
853        class: "moss-embed-pdf",
854        kind: "instance",
855        parent: "moss-embed",
856        data_attrs: &[],
857        example_html: r#"<div class="moss-embed moss-embed-pdf"><iframe src="..."></iframe></div>"#,
858        example_markdown: "![[paper.pdf]]",
859        status: Status::Retired,
860        since: "0",
861        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=pdf]`.",
862    },
863    ComponentEntry {
864        class: "moss-embed-iframe",
865        kind: "instance",
866        parent: "moss-embed",
867        data_attrs: &[],
868        example_html: r#"<div class="moss-embed moss-embed-iframe"><iframe src="..."></iframe></div>"#,
869        example_markdown: "![[page.html]]",
870        status: Status::Retired,
871        since: "0",
872        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=iframe]`.",
873    },
874    ComponentEntry {
875        class: "moss-embed-notebook",
876        kind: "instance",
877        parent: "moss-embed",
878        data_attrs: &[],
879        example_html: r#"<div class="moss-embed moss-embed-notebook">...</div>"#,
880        example_markdown: "![[analysis.ipynb]]",
881        status: Status::Retired,
882        since: "0",
883        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=notebook]`.",
884    },
885    ComponentEntry {
886        class: "moss-embed-ipynb",
887        kind: "instance",
888        parent: "moss-embed",
889        data_attrs: &[],
890        example_html: r#"<div class="moss-embed moss-embed-ipynb">...</div>"#,
891        example_markdown: "",
892        status: Status::Emerging,
893        since: "0",
894        description: "Alias of `.moss-embed-notebook`; consolidation pending.",
895    },
896    ComponentEntry {
897        class: "moss-embed-table",
898        kind: "instance",
899        parent: "moss-embed",
900        data_attrs: &[],
901        example_html: r#"<div class="moss-embed moss-embed-table"><table>...</table></div>"#,
902        example_markdown: "![[data.csv]]",
903        status: Status::Retired,
904        since: "0",
905        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=table]`.",
906    },
907    ComponentEntry {
908        class: "moss-embed-3d",
909        kind: "instance",
910        parent: "moss-embed",
911        data_attrs: &[],
912        example_html: r#"<div class="moss-embed moss-embed-3d">...</div>"#,
913        example_markdown: "![[model.glb]]",
914        status: Status::Retired,
915        since: "0",
916        description: "Retired in Phase 1c — collapsed to `.moss-embed[data-type=3d]`.",
917    },
918    ComponentEntry {
919        class: "moss-embed-error",
920        kind: "instance",
921        parent: "moss-embed",
922        data_attrs: &[],
923        example_html: r#"<div class="moss-embed moss-embed-error">File not found: ...</div>"#,
924        example_markdown: "",
925        status: Status::Confirmed,
926        since: "0",
927        description: "Error state for embeds whose target cannot be resolved.",
928    },
929    ComponentEntry {
930        class: "moss-embed-missing",
931        kind: "instance",
932        parent: "moss-embed",
933        data_attrs: &[],
934        example_html: r#"<div class="moss-embed-missing">Folder not found: journal</div>"#,
935        example_markdown: "",
936        status: Status::Confirmed,
937        since: "1",
938        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.",
939    },
940    // -------------------------------------------------------------------
941    // Hero, image, visual primitives.
942    // -------------------------------------------------------------------
943    ComponentEntry {
944        class: "moss-hero",
945        kind: "standalone",
946        parent: "",
947        data_attrs: &[
948            DataAttr {
949                name: "data-width",
950                values: &["body", "wide", "page", "screen"],
951                default: "body",
952                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9. Phase 1c will emit this from authoring shortcode (e.g. `:::hero {full}` -> `data-width=\"screen\"`).",
953            },
954        ],
955        example_html: r#"<section class="moss-hero" data-width="page">
956  <div class="moss-hero-content">...</div>
957</section>"#,
958        example_markdown: ":::hero {image=cover.jpg}\n:::\n",
959        status: Status::Confirmed,
960        since: "0",
961        description: "Hero banner section at the top of a page (cover image + title). v1 adds `data-width` for author-controlled sizing.",
962    },
963    ComponentEntry {
964        class: "moss-hero-content",
965        kind: "instance",
966        parent: "moss-hero",
967        data_attrs: &[],
968        example_html: r#"<div class="moss-hero-content">...</div>"#,
969        example_markdown: "",
970        status: Status::Confirmed,
971        since: "0",
972        description: "Text content slot inside `.moss-hero`.",
973    },
974    ComponentEntry {
975        class: "moss-image",
976        kind: "standalone",
977        parent: "",
978        data_attrs: &[
979            DataAttr {
980                name: "data-aspect",
981                values: &["portrait", "square", "auto"],
982                default: "auto",
983                description: "v1 image aspect-ratio hint. Theme authors target `.moss-image[data-aspect=...]`. Emitter wiring lands in a follow-up.",
984            },
985            DataAttr {
986                name: "data-width",
987                values: &["body", "wide", "page", "screen"],
988                default: "body",
989                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
990            },
991        ],
992        example_html: r#"<figure class="moss-image" style="width:55%"><img src="..." alt="..." /></figure>"#,
993        example_markdown: "![alt](image.jpg)",
994        status: Status::Confirmed,
995        since: "0",
996        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.",
997    },
998    ComponentEntry {
999        class: "moss-align-left",
1000        kind: "standalone",
1001        parent: "",
1002        data_attrs: &[],
1003        example_html: r#"<img src="..." alt="..." class="moss-align-left" />"#,
1004        example_markdown: "![[photo.jpg|align-left]]",
1005        status: Status::Confirmed,
1006        since: "0",
1007        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.",
1008    },
1009    ComponentEntry {
1010        class: "moss-align-right",
1011        kind: "standalone",
1012        parent: "",
1013        data_attrs: &[],
1014        example_html: r#"<img src="..." alt="..." class="moss-align-right" />"#,
1015        example_markdown: "![[photo.jpg|align-right]]",
1016        status: Status::Confirmed,
1017        since: "0",
1018        description: "Floats an image to the right of body text (editorial runaround). Symmetric counterpart to `.moss-align-left`. Mirrors WordPress's `alignright` convention.",
1019    },
1020    ComponentEntry {
1021        class: "moss-article-title",
1022        kind: "instance",
1023        parent: "",
1024        data_attrs: &[],
1025        example_html: r#"<h1 class="moss-article-title">Title</h1>"#,
1026        example_markdown: "",
1027        status: Status::Emerging,
1028        since: "0",
1029        description: "Article-page H1 title emitted from frontmatter.",
1030    },
1031    ComponentEntry {
1032        class: "moss-heading-anchor",
1033        kind: "instance",
1034        parent: "",
1035        data_attrs: &[],
1036        example_html: r##"<h2 id="setup">Setup<a class="moss-heading-anchor" href="#setup" aria-label="Permalink to this section"><span aria-hidden="true">#</span></a></h2>"##,
1037        example_markdown: "## Setup",
1038        status: Status::Emerging,
1039        since: "1",
1040        description: "Clickable permalink appended inside every author-written body heading that carries a slug id; links to the heading's `#`-fragment. The auto-injected `moss-article-title` H1 is emitted separately and gets no anchor.",
1041    },
1042    // -------------------------------------------------------------------
1043    // Grid + gallery + buttons containers (free-form layouts).
1044    // -------------------------------------------------------------------
1045    ComponentEntry {
1046        class: "moss-grid",
1047        kind: "container",
1048        parent: "",
1049        data_attrs: &[
1050            DataAttr {
1051                name: "data-width",
1052                values: &["body", "wide", "page", "screen"],
1053                default: "body",
1054                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1055            },
1056        ],
1057        example_html: r#"<div class="moss-grid" data-width="wide">
1058  <div class="moss-grid-card">...</div>
1059</div>"#,
1060        example_markdown: ":::grid {cols=2}\nLeft cell\n+++\nRight cell\n:::\n",
1061        status: Status::Confirmed,
1062        since: "0",
1063        description: "Generic grid container (used by profiles, link previews, etc.). Modifier classes: `profiles`, `featured`, `no-cards`. v1 adds `data-width` (P9).",
1064    },
1065    ComponentEntry {
1066        class: "moss-grid-card",
1067        kind: "instance",
1068        parent: "moss-grid",
1069        data_attrs: &[
1070            DataAttr {
1071                name: "data-kind",
1072                values: &["link", "friend", "card"],
1073                default: "card",
1074                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.",
1075            },
1076        ],
1077        example_html: r#"<a class="moss-grid-card" data-kind="link" href="...">...</a>"#,
1078        example_markdown: "",
1079        status: Status::Confirmed,
1080        since: "0",
1081        description: "Card instance inside `.moss-grid`. Today emits sibling classes `link-card` / `friend-card` / `no-cards`; v1 collapses to `data-kind`.",
1082    },
1083    ComponentEntry {
1084        class: "moss-gallery",
1085        kind: "container",
1086        parent: "",
1087        data_attrs: &[
1088            DataAttr {
1089                name: "data-width",
1090                values: &["body", "wide", "page", "screen"],
1091                default: "body",
1092                description: "Display width — text-column (body), wider than text (wide), page-width (page), or viewport-width (screen). See spec § P9.",
1093            },
1094        ],
1095        example_html: r#"<div class="moss-gallery" data-width="page">
1096  <figure class="moss-gallery-item">...</figure>
1097</div>"#,
1098        example_markdown: ":::gallery\nphoto.jpg\n:::\n",
1099        status: Status::Confirmed,
1100        since: "0",
1101        description: "Image gallery container. v1 adds `data-width` (P9).",
1102    },
1103    ComponentEntry {
1104        class: "moss-gallery-item",
1105        kind: "instance",
1106        parent: "moss-gallery",
1107        data_attrs: &[],
1108        example_html: r#"<figure class="moss-gallery-item"><img src="..." /></figure>"#,
1109        example_markdown: "",
1110        status: Status::Confirmed,
1111        since: "0",
1112        description: "Single image entry inside `.moss-gallery`.",
1113    },
1114    ComponentEntry {
1115        class: "moss-buttons",
1116        kind: "container",
1117        parent: "",
1118        data_attrs: &[
1119            DataAttr {
1120                name: "data-style",
1121                values: &["default", "inverted"],
1122                default: "default",
1123                description: "v1 button-row style. Theme authors target `.moss-buttons[data-style=...]`.",
1124            },
1125        ],
1126        example_html: r#"<div class="moss-buttons" data-style="inverted">
1127  <a class="moss-btn" href="...">Click</a>
1128</div>"#,
1129        example_markdown: ":::buttons\n[Get started](https://example.com)\n:::\n",
1130        status: Status::Confirmed,
1131        since: "0",
1132        description: "Container for a row of `.moss-btn` buttons. v1: the inverted variant is on `data-style=\"inverted\"`.",
1133    },
1134    // -------------------------------------------------------------------
1135    // Button primitive (used by subscribe + general CTAs).
1136    // -------------------------------------------------------------------
1137    ComponentEntry {
1138        class: "moss-btn",
1139        kind: "standalone",
1140        parent: "",
1141        data_attrs: &[
1142            DataAttr {
1143                name: "data-role",
1144                values: &["default", "primary", "secondary"],
1145                default: "default",
1146                description: "v1 button role. Theme authors target `.moss-btn[data-role=...]`.",
1147            },
1148        ],
1149        example_html: r#"<button class="moss-btn" data-role="primary">
1150  <span class="moss-btn__label">Submit</span>
1151</button>"#,
1152        example_markdown: "",
1153        status: Status::Confirmed,
1154        since: "0",
1155        description: "Generic button primitive. Role on `data-role` (v1).",
1156    },
1157    ComponentEntry {
1158        class: "moss-btn__label",
1159        kind: "instance",
1160        parent: "moss-btn",
1161        data_attrs: &[],
1162        example_html: r#"<span class="moss-btn__label">Submit</span>"#,
1163        example_markdown: "",
1164        status: Status::Confirmed,
1165        since: "0",
1166        description: "Label span inside `.moss-btn`.",
1167    },
1168    ComponentEntry {
1169        class: "moss-btn__check",
1170        kind: "instance",
1171        parent: "moss-btn",
1172        data_attrs: &[],
1173        example_html: r#"<span class="moss-btn__check">✓</span>"#,
1174        example_markdown: "",
1175        status: Status::Confirmed,
1176        since: "0",
1177        description: "Success checkmark slot inside `.moss-btn`.",
1178    },
1179    ComponentEntry {
1180        class: "moss-btn__spinner",
1181        kind: "instance",
1182        parent: "moss-btn",
1183        data_attrs: &[],
1184        example_html: r#"<span class="moss-btn__spinner"></span>"#,
1185        example_markdown: "",
1186        status: Status::Confirmed,
1187        since: "0",
1188        description: "Loading spinner slot inside `.moss-btn`.",
1189    },
1190    // -------------------------------------------------------------------
1191    // Subscribe form (newsletter / Buttondown / seta).
1192    // -------------------------------------------------------------------
1193    ComponentEntry {
1194        class: "moss-subscribe",
1195        kind: "standalone",
1196        parent: "",
1197        data_attrs: &[],
1198        example_html: r#"<div class="moss-subscribe">
1199  <form class="moss-subscribe-form">...</form>
1200</div>"#,
1201        example_markdown: ":::subscribe\n:::\n",
1202        status: Status::Confirmed,
1203        since: "0",
1204        description: "Newsletter subscribe block (auto-injected into footer when email channel configured).",
1205    },
1206    ComponentEntry {
1207        class: "moss-subscribe-form",
1208        kind: "instance",
1209        parent: "moss-subscribe",
1210        data_attrs: &[
1211            DataAttr {
1212                name: "data-position",
1213                values: &["inline", "apply"],
1214                default: "inline",
1215                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).",
1216            },
1217            DataAttr {
1218                name: "data-button-override",
1219                values: &["true"],
1220                default: "true",
1221                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.",
1222            },
1223            DataAttr {
1224                name: "data-moss-hosted",
1225                values: &["true"],
1226                default: "true",
1227                description: "Marks moss-hosted (seta) forms hydrated by subscribe.ts. Absent on 3rd-party provider forms.",
1228            },
1229            DataAttr {
1230                name: "data-state",
1231                values: &["idle", "loading", "success", "error"],
1232                default: "idle",
1233                description: "Runtime submit state machine, driven by subscribe.ts. Emitted as `idle`; theme authors target `.moss-subscribe-form[data-state=...]`.",
1234            },
1235            DataAttr {
1236                name: "data-moss-pending-site",
1237                values: &["true"],
1238                default: "true",
1239                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.",
1240            },
1241        ],
1242        example_html: r#"<form class="moss-subscribe-form">...</form>"#,
1243        example_markdown: "",
1244        status: Status::Emerging,
1245        since: "0",
1246        description: "Form element inside `.moss-subscribe`.",
1247    },
1248    ComponentEntry {
1249        class: "moss-btn-slot",
1250        kind: "instance",
1251        parent: "moss-subscribe",
1252        data_attrs: &[],
1253        example_html: r#"<div class="moss-btn-slot"><button class="moss-btn">...</button></div>"#,
1254        example_markdown: "",
1255        status: Status::Emerging,
1256        since: "0",
1257        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.",
1258    },
1259    ComponentEntry {
1260        class: "moss-subscribe-status",
1261        kind: "instance",
1262        parent: "moss-subscribe",
1263        data_attrs: &[],
1264        example_html: r#"<div class="moss-subscribe-status">
1265  <span class="moss-subscribe-status__icon"></span>
1266  Subscribed!
1267</div>"#,
1268        example_markdown: "",
1269        status: Status::Emerging,
1270        since: "0",
1271        description: "Status message shown after submit (success/error).",
1272    },
1273    ComponentEntry {
1274        class: "moss-subscribe-status__icon",
1275        kind: "instance",
1276        parent: "moss-subscribe-status",
1277        data_attrs: &[],
1278        example_html: r#"<span class="moss-subscribe-status__icon"></span>"#,
1279        example_markdown: "",
1280        status: Status::Emerging,
1281        since: "0",
1282        description: "Icon slot inside `.moss-subscribe-status`.",
1283    },
1284    ComponentEntry {
1285        class: "moss-subscribe-landing",
1286        kind: "standalone",
1287        parent: "",
1288        data_attrs: &[],
1289        example_html: r#"<section class="moss-subscribe-landing">...</section>"#,
1290        example_markdown: "",
1291        status: Status::Emerging,
1292        since: "0",
1293        description: "Standalone subscribe landing page surface (larger variant).",
1294    },
1295    // -------------------------------------------------------------------
1296    // Apply form (membership / contributor application).
1297    // -------------------------------------------------------------------
1298    ComponentEntry {
1299        class: "moss-apply",
1300        kind: "standalone",
1301        parent: "",
1302        data_attrs: &[],
1303        example_html: r#"<div class="moss-apply" data-state="idle">
1304  <form class="moss-subscribe-form moss-apply-form">...</form>
1305</div>"#,
1306        example_markdown: ":::apply\n:::\n",
1307        status: Status::Emerging,
1308        since: "0",
1309        description: "Apply / membership-request form block (:::apply shortcode).",
1310    },
1311    ComponentEntry {
1312        class: "moss-apply-form",
1313        kind: "instance",
1314        parent: "moss-apply",
1315        data_attrs: &[
1316            DataAttr {
1317                name: "data-position",
1318                values: &["apply"],
1319                default: "apply",
1320                description: "Position variant; always `apply` for this form. Drives CSS layout in email.css.",
1321            },
1322            DataAttr {
1323                name: "data-revert",
1324                values: &["false"],
1325                default: "false",
1326                description: "When `false`, success is terminal (no auto-revert). subscribe.ts reads this.",
1327            },
1328        ],
1329        example_html: r#"<form class="moss-subscribe-form moss-apply-form" data-position="apply" data-revert="false">...</form>"#,
1330        example_markdown: "",
1331        status: Status::Emerging,
1332        since: "0",
1333        description: "Form element inside `.moss-apply`. Also carries `.moss-subscribe-form` so subscribe.ts hydrates it.",
1334    },
1335    ComponentEntry {
1336        class: "moss-apply-matters",
1337        kind: "instance",
1338        parent: "moss-apply",
1339        data_attrs: &[],
1340        example_html: r#"<input type="text" name="matters" class="moss-input moss-apply-matters">"#,
1341        example_markdown: "",
1342        status: Status::Emerging,
1343        since: "0",
1344        description: "Second apply-form input inside `.moss-apply-form` — a Matters username OR a one-line pitch (placeholder-only, no visible label).",
1345    },
1346    ComponentEntry {
1347        class: "moss-apply-hp",
1348        kind: "instance",
1349        parent: "moss-apply",
1350        data_attrs: &[],
1351        example_html: r#"<input type="text" name="website" class="moss-apply-hp" tabindex="-1" aria-hidden="true">"#,
1352        example_markdown: "",
1353        status: Status::Emerging,
1354        since: "0",
1355        description: "Honeypot field (off-screen) inside `.moss-apply-form`. Bots fill it; humans don't.",
1356    },
1357    ComponentEntry {
1358        class: "moss-apply-status",
1359        kind: "instance",
1360        parent: "moss-apply",
1361        data_attrs: &[],
1362        example_html: r#"<div class="moss-subscribe-status moss-apply-status" aria-live="polite">...</div>"#,
1363        example_markdown: "",
1364        status: Status::Emerging,
1365        since: "0",
1366        description: "Status region inside `.moss-apply-form` (also carries `.moss-subscribe-status`).",
1367    },
1368    ComponentEntry {
1369        class: "moss-apply-helper",
1370        kind: "instance",
1371        parent: "moss-apply",
1372        data_attrs: &[],
1373        example_html: r#"<p class="moss-apply-helper" id="moss-apply-email-help">用于获取邀请及免费托管服务</p>"#,
1374        example_markdown: "",
1375        status: Status::Emerging,
1376        since: "0",
1377        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.",
1378    },
1379    // -------------------------------------------------------------------
1380    // Series navigation (prev/next + collection links).
1381    // -------------------------------------------------------------------
1382    ComponentEntry {
1383        class: "moss-series-nav",
1384        kind: "standalone",
1385        parent: "",
1386        data_attrs: &[],
1387        example_html: r#"<nav class="moss-series-nav">
1388  <div class="moss-series-nav-links">...</div>
1389</nav>"#,
1390        example_markdown: "",
1391        status: Status::Confirmed,
1392        since: "0",
1393        description: "Series navigation bar (prev/next/collection) on series pages.",
1394    },
1395    ComponentEntry {
1396        class: "moss-series-nav-links",
1397        kind: "instance",
1398        parent: "moss-series-nav",
1399        data_attrs: &[],
1400        example_html: r#"<div class="moss-series-nav-links">...</div>"#,
1401        example_markdown: "",
1402        status: Status::Confirmed,
1403        since: "0",
1404        description: "Row holding prev/next links in series nav.",
1405    },
1406    ComponentEntry {
1407        class: "moss-series-nav-link",
1408        kind: "instance",
1409        parent: "moss-series-nav",
1410        data_attrs: &[],
1411        example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1412        example_markdown: "",
1413        status: Status::Confirmed,
1414        since: "0",
1415        description: "Individual link inside series nav. Modifiers: `moss-series-nav-prev`, `moss-series-nav-next`, `empty` (placeholder).",
1416    },
1417    ComponentEntry {
1418        class: "moss-series-nav-prev",
1419        kind: "instance",
1420        parent: "moss-series-nav",
1421        data_attrs: &[],
1422        example_html: r#"<a class="moss-series-nav-link moss-series-nav-prev" href="...">...</a>"#,
1423        example_markdown: "",
1424        status: Status::Confirmed,
1425        since: "0",
1426        description: "Previous-page modifier on a series nav link.",
1427    },
1428    ComponentEntry {
1429        class: "moss-series-nav-next",
1430        kind: "instance",
1431        parent: "moss-series-nav",
1432        data_attrs: &[],
1433        example_html: r#"<a class="moss-series-nav-link moss-series-nav-next" href="...">...</a>"#,
1434        example_markdown: "",
1435        status: Status::Confirmed,
1436        since: "0",
1437        description: "Next-page modifier on a series nav link.",
1438    },
1439    ComponentEntry {
1440        class: "moss-series-nav-arrow",
1441        kind: "instance",
1442        parent: "moss-series-nav",
1443        data_attrs: &[],
1444        example_html: r#"<span class="moss-series-nav-arrow">→</span>"#,
1445        example_markdown: "",
1446        status: Status::Confirmed,
1447        since: "0",
1448        description: "Arrow glyph inside a series-nav link.",
1449    },
1450    ComponentEntry {
1451        class: "moss-series-nav-title",
1452        kind: "instance",
1453        parent: "moss-series-nav",
1454        data_attrs: &[],
1455        example_html: r#"<span class="moss-series-nav-title">Next page title</span>"#,
1456        example_markdown: "",
1457        status: Status::Confirmed,
1458        since: "0",
1459        description: "Title text of the destination page in a series-nav link.",
1460    },
1461    ComponentEntry {
1462        class: "moss-series-nav-collection",
1463        kind: "instance",
1464        parent: "moss-series-nav",
1465        data_attrs: &[],
1466        example_html: r#"<div class="moss-series-nav-collection">...</div>"#,
1467        example_markdown: "",
1468        status: Status::Confirmed,
1469        since: "0",
1470        description: "Collection-listing slot in series nav (sibling pages).",
1471    },
1472    ComponentEntry {
1473        class: "moss-series-nav-collection-row",
1474        kind: "instance",
1475        parent: "moss-series-nav-collection",
1476        data_attrs: &[],
1477        example_html: r#"<div class="moss-series-nav-collection-row">...</div>"#,
1478        example_markdown: "",
1479        status: Status::Confirmed,
1480        since: "0",
1481        description: "Row inside the collection listing of series nav.",
1482    },
1483    // -------------------------------------------------------------------
1484    // Collection cover (collection landing pages).
1485    // -------------------------------------------------------------------
1486    ComponentEntry {
1487        class: "moss-collection-cover",
1488        kind: "standalone",
1489        parent: "",
1490        data_attrs: &[],
1491        example_html: r#"<section class="moss-collection-cover">
1492  <div class="moss-collection-cover-row">...</div>
1493</section>"#,
1494        example_markdown: "",
1495        status: Status::Emerging,
1496        since: "0",
1497        description: "Header surface on a collection landing page.",
1498    },
1499    ComponentEntry {
1500        class: "moss-collection-cover-row",
1501        kind: "instance",
1502        parent: "moss-collection-cover",
1503        data_attrs: &[],
1504        example_html: r#"<div class="moss-collection-cover-row">...</div>"#,
1505        example_markdown: "",
1506        status: Status::Emerging,
1507        since: "0",
1508        description: "Row inside `.moss-collection-cover`.",
1509    },
1510    ComponentEntry {
1511        class: "moss-collection-cover-body",
1512        kind: "instance",
1513        parent: "moss-collection-cover",
1514        data_attrs: &[],
1515        example_html: r#"<div class="moss-collection-cover-body">...</div>"#,
1516        example_markdown: "",
1517        status: Status::Emerging,
1518        since: "0",
1519        description: "Body content slot inside `.moss-collection-cover`.",
1520    },
1521    // -------------------------------------------------------------------
1522    // Form primitives (input, label, field, link).
1523    // -------------------------------------------------------------------
1524    ComponentEntry {
1525        class: "moss-input",
1526        kind: "standalone",
1527        parent: "",
1528        data_attrs: &[],
1529        example_html: r#"<input class="moss-input" type="email" />"#,
1530        example_markdown: "",
1531        status: Status::Confirmed,
1532        since: "0",
1533        description: "Generic form input primitive.",
1534    },
1535    ComponentEntry {
1536        class: "moss-field",
1537        kind: "container",
1538        parent: "",
1539        data_attrs: &[],
1540        example_html: r#"<div class="moss-field">
1541  <label class="moss-label">Email</label>
1542  <input class="moss-input" />
1543</div>"#,
1544        example_markdown: "",
1545        status: Status::Confirmed,
1546        since: "0",
1547        description: "Form field group (label + input). Modifier `--inline` for horizontal layout.",
1548    },
1549    ComponentEntry {
1550        class: "moss-label",
1551        kind: "instance",
1552        parent: "moss-field",
1553        data_attrs: &[],
1554        example_html: r#"<label class="moss-label">Email</label>"#,
1555        example_markdown: "",
1556        status: Status::Confirmed,
1557        since: "0",
1558        description: "Label primitive for `.moss-field`. Modifier `--small` for compact form.",
1559    },
1560    ComponentEntry {
1561        class: "moss-link",
1562        kind: "standalone",
1563        parent: "",
1564        data_attrs: &[],
1565        example_html: r#"<a class="moss-link" href="...">Click me</a>"#,
1566        example_markdown: "",
1567        status: Status::Confirmed,
1568        since: "0",
1569        description: "Inline-link primitive (resets `<button>` chrome too). Use `--subtle` for muted variant.",
1570    },
1571    ComponentEntry {
1572        class: "moss-field--inline",
1573        kind: "instance",
1574        parent: "moss-field",
1575        data_attrs: &[],
1576        example_html: r#"<div class="moss-field moss-field--inline">
1577  <label class="moss-label">Email</label>
1578  <input class="moss-input" />
1579</div>"#,
1580        example_markdown: "",
1581        status: Status::Confirmed,
1582        since: "0",
1583        description: "BEM modifier on `.moss-field` for horizontal label+input layout (used by settings UI primitives).",
1584    },
1585    ComponentEntry {
1586        class: "moss-label--small",
1587        kind: "instance",
1588        parent: "moss-label",
1589        data_attrs: &[],
1590        example_html: r#"<label class="moss-label moss-label--small">Compact label</label>"#,
1591        example_markdown: "",
1592        status: Status::Confirmed,
1593        since: "0",
1594        description: "BEM modifier on `.moss-label` for compact form (used by services settings rows).",
1595    },
1596    ComponentEntry {
1597        class: "moss-info-grid",
1598        kind: "container",
1599        parent: "",
1600        data_attrs: &[],
1601        example_html: r#"<div class="moss-info-grid">
1602  <div class="moss-field moss-field--inline">...</div>
1603  <div class="moss-field moss-field--inline">...</div>
1604</div>"#,
1605        example_markdown: "",
1606        status: Status::Emerging,
1607        since: "0",
1608        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.",
1609    },
1610    ComponentEntry {
1611        class: "moss-row",
1612        kind: "container",
1613        parent: "",
1614        data_attrs: &[],
1615        example_html: r#"<div class="moss-row">
1616  <div class="moss-field">...</div>
1617  <div class="moss-field">...</div>
1618</div>"#,
1619        example_markdown: "",
1620        status: Status::Emerging,
1621        since: "0",
1622        description: "Horizontal flex row of equal-flex `.moss-field` children. Form-row layout helper shipped in the default theme.",
1623    },
1624    ComponentEntry {
1625        class: "moss-input-feedback",
1626        kind: "instance",
1627        parent: "moss-field",
1628        data_attrs: &[],
1629        example_html: r#"<span class="moss-input-feedback">Saving…</span>"#,
1630        example_markdown: "",
1631        status: Status::Emerging,
1632        since: "0",
1633        description: "Auto-save status hint slot under `.moss-field`. Three state modifiers: `--success`, `--error`, `--fade-out`.",
1634    },
1635    ComponentEntry {
1636        class: "moss-input-feedback--success",
1637        kind: "instance",
1638        parent: "moss-input-feedback",
1639        data_attrs: &[],
1640        example_html: r#"<span class="moss-input-feedback moss-input-feedback--success">Saved</span>"#,
1641        example_markdown: "",
1642        status: Status::Emerging,
1643        since: "0",
1644        description: "Success state modifier on `.moss-input-feedback`.",
1645    },
1646    ComponentEntry {
1647        class: "moss-input-feedback--error",
1648        kind: "instance",
1649        parent: "moss-input-feedback",
1650        data_attrs: &[],
1651        example_html: r#"<span class="moss-input-feedback moss-input-feedback--error">Failed to save</span>"#,
1652        example_markdown: "",
1653        status: Status::Emerging,
1654        since: "0",
1655        description: "Error state modifier on `.moss-input-feedback`.",
1656    },
1657    ComponentEntry {
1658        class: "moss-input-feedback--fade-out",
1659        kind: "instance",
1660        parent: "moss-input-feedback",
1661        data_attrs: &[],
1662        example_html: r#"<span class="moss-input-feedback moss-input-feedback--success moss-input-feedback--fade-out">Saved</span>"#,
1663        example_markdown: "",
1664        status: Status::Emerging,
1665        since: "0",
1666        description: "Transient fade-out modifier on `.moss-input-feedback` (applied after a success message to dismiss it).",
1667    },
1668    // -------------------------------------------------------------------
1669    // Other emit surfaces (comments, colophon, shell frame, misc).
1670    // -------------------------------------------------------------------
1671    ComponentEntry {
1672        class: "moss-comments",
1673        kind: "standalone",
1674        parent: "",
1675        data_attrs: &[],
1676        example_html: r#"<section class="moss-comments">...</section>"#,
1677        example_markdown: "",
1678        status: Status::Confirmed,
1679        since: "0",
1680        description: "Comments surface (per-site SQLite backend or Artalk legacy).",
1681    },
1682    ComponentEntry {
1683        class: "moss-service-inactive",
1684        kind: "instance",
1685        parent: "",
1686        data_attrs: &[],
1687        example_html: r#"<section class="moss-comments moss-service-inactive">...</section>"#,
1688        example_markdown: "",
1689        status: Status::Confirmed,
1690        since: "0",
1691        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.",
1692    },
1693    // -------------------------------------------------------------------
1694    // Preview link popover — emitted by `assets/js/preview.js` runtime.
1695    // -------------------------------------------------------------------
1696    ComponentEntry {
1697        class: "moss-preview-popup",
1698        kind: "chrome",
1699        parent: "",
1700        data_attrs: &[],
1701        example_html: r#"<div class="moss-preview-popup" role="tooltip" aria-live="polite">
1702  <strong class="moss-preview-title">...</strong>
1703  <p class="moss-preview-desc">...</p>
1704  <p class="moss-preview-text">...</p>
1705</div>"#,
1706        example_markdown: "",
1707        status: Status::Confirmed,
1708        since: "0",
1709        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.",
1710    },
1711    ComponentEntry {
1712        class: "moss-preview-title",
1713        kind: "instance",
1714        parent: "moss-preview-popup",
1715        data_attrs: &[],
1716        example_html: r#"<strong class="moss-preview-title">Article title</strong>"#,
1717        example_markdown: "",
1718        status: Status::Confirmed,
1719        since: "0",
1720        description: "Title slot inside `.moss-preview-popup`.",
1721    },
1722    ComponentEntry {
1723        class: "moss-preview-desc",
1724        kind: "instance",
1725        parent: "moss-preview-popup",
1726        data_attrs: &[],
1727        example_html: r#"<p class="moss-preview-desc">Short description</p>"#,
1728        example_markdown: "",
1729        status: Status::Confirmed,
1730        since: "0",
1731        description: "Description slot inside `.moss-preview-popup` (from frontmatter `description`).",
1732    },
1733    ComponentEntry {
1734        class: "moss-preview-text",
1735        kind: "instance",
1736        parent: "moss-preview-popup",
1737        data_attrs: &[],
1738        example_html: r#"<p class="moss-preview-text">Excerpt of the linked article…</p>"#,
1739        example_markdown: "",
1740        status: Status::Confirmed,
1741        since: "0",
1742        description: "Excerpt slot inside `.moss-preview-popup` (auto-extracted from the linked article body).",
1743    },
1744    // -------------------------------------------------------------------
1745    // Missing-image fallback marker — added by the inline
1746    // `#moss-img-fallback` script in shell.html at runtime, when an <img>
1747    // fails to load.
1748    // -------------------------------------------------------------------
1749    ComponentEntry {
1750        class: "moss-img-fallback",
1751        kind: "chrome",
1752        parent: "",
1753        data_attrs: &[],
1754        example_html: r#"<img class="site-logo moss-img-fallback" src="data:image/svg+xml,..." alt="" aria-hidden="true">"#,
1755        example_markdown: "",
1756        status: Status::Confirmed,
1757        since: "0",
1758        description: "Marker class a capture-phase `error` listener (inlined in shell.html, not a separate hashed asset) adds to any <img> whose load fails (deleted/renamed/typo'd source — never enters moss's AssetRegistry, so nothing server-side can placeholder it). The browser's native broken-image icon never appears: the script swaps the <img>'s OWN `src` in place to a self-contained blueprint-grid-pattern SVG data URI (same blueprint-blue as the animated frontend/app/components/blueprint-grid.ts canvas, without the per-instance canvas/RAF cost) and strips any enclosing <picture>'s <source> children — it does NOT replace the element, so every context-specific sizing/fit rule (.moss-card-cover > img, .moss-hero img, .site-logo, …) keeps applying because the <img>'s tag, class list, and other attributes are untouched.",
1759    },
1760    ComponentEntry {
1761        class: "moss-colophon",
1762        kind: "chrome",
1763        parent: "",
1764        data_attrs: &[],
1765        example_html: r#"<footer class="moss-colophon">
1766  <span class="moss-colophon-icon"></span>
1767  Built with moss
1768</footer>"#,
1769        example_markdown: "",
1770        status: Status::Confirmed,
1771        since: "0",
1772        description: "Footer colophon credit appended by moss.",
1773    },
1774    ComponentEntry {
1775        class: "moss-colophon-icon",
1776        kind: "instance",
1777        parent: "moss-colophon",
1778        data_attrs: &[],
1779        example_html: r#"<span class="moss-colophon-icon"></span>"#,
1780        example_markdown: "",
1781        status: Status::Confirmed,
1782        since: "0",
1783        description: "Icon slot inside `.moss-colophon`.",
1784    },
1785    ComponentEntry {
1786        class: "moss-shell-frame",
1787        kind: "chrome",
1788        parent: "",
1789        data_attrs: &[],
1790        example_html: r#"<div class="moss-shell-frame">...</div>"#,
1791        example_markdown: "",
1792        status: Status::Emerging,
1793        since: "0",
1794        description: "App-shell frame surface (preview chrome).",
1795    },
1796    ComponentEntry {
1797        class: "moss-mobile-frame",
1798        kind: "chrome",
1799        parent: "moss-shell-frame",
1800        data_attrs: &[],
1801        example_html: r#"<html class="moss-shell-frame moss-mobile-frame">...</html>"#,
1802        example_markdown: "",
1803        status: Status::Emerging,
1804        since: "0",
1805        description: "Runtime marker the preview bridge adds to `<html>` when the shell is in mobile device-preview mode; drops the titlebar-clearance padding (the phone frame sits below the titlebar).",
1806    },
1807    ComponentEntry {
1808        class: "main-nav",
1809        kind: "chrome",
1810        parent: "",
1811        data_attrs: &[],
1812        example_html: r#"<nav class="main-nav container">...</nav>"#,
1813        example_markdown: "",
1814        status: Status::Confirmed,
1815        since: "0",
1816        description: "Top site navigation bar. Legacy non-`moss-` prefix kept for theme parity.",
1817    },
1818    ComponentEntry {
1819        class: "moss-child-section-divider",
1820        kind: "instance",
1821        parent: "",
1822        data_attrs: &[],
1823        example_html: r#"<hr class="moss-child-section-divider" />"#,
1824        example_markdown: "",
1825        status: Status::Emerging,
1826        since: "0",
1827        description: "Divider rule between auto-generated child sections.",
1828    },
1829    ComponentEntry {
1830        class: "moss-unknown-shortcode",
1831        kind: "standalone",
1832        parent: "",
1833        data_attrs: &[],
1834        example_html: r#"<div class="moss-unknown-shortcode">Unknown shortcode: foo</div>"#,
1835        example_markdown: "{{< foo >}}",
1836        status: Status::Confirmed,
1837        since: "0",
1838        description: "Fallback emitted when a shortcode tag is not recognised by any plugin.",
1839    },
1840    // -------------------------------------------------------------------
1841    // Syntax highlight tokens (emitted by syntect inside <code>).
1842    // -------------------------------------------------------------------
1843    ComponentEntry {
1844        class: "moss-hl-keyword",
1845        kind: "instance",
1846        parent: "",
1847        data_attrs: &[],
1848        example_html: r#"<span class="moss-hl-keyword">if</span>"#,
1849        example_markdown: "",
1850        status: Status::Emerging,
1851        since: "0",
1852        description: "Syntax-highlight token: keyword.",
1853    },
1854    ComponentEntry {
1855        class: "moss-hl-string",
1856        kind: "instance",
1857        parent: "",
1858        data_attrs: &[],
1859        example_html: r#"<span class="moss-hl-string">"hi"</span>"#,
1860        example_markdown: "",
1861        status: Status::Emerging,
1862        since: "0",
1863        description: "Syntax-highlight token: string literal.",
1864    },
1865    ComponentEntry {
1866        class: "moss-hl-comment",
1867        kind: "instance",
1868        parent: "",
1869        data_attrs: &[],
1870        example_html: r#"<span class="moss-hl-comment">// note</span>"#,
1871        example_markdown: "",
1872        status: Status::Emerging,
1873        since: "0",
1874        description: "Syntax-highlight token: comment.",
1875    },
1876    ComponentEntry {
1877        class: "moss-hl-function",
1878        kind: "instance",
1879        parent: "",
1880        data_attrs: &[],
1881        example_html: r#"<span class="moss-hl-function">render</span>"#,
1882        example_markdown: "",
1883        status: Status::Emerging,
1884        since: "0",
1885        description: "Syntax-highlight token: function name.",
1886    },
1887    ComponentEntry {
1888        class: "moss-hl-type",
1889        kind: "instance",
1890        parent: "",
1891        data_attrs: &[],
1892        example_html: r#"<span class="moss-hl-type">String</span>"#,
1893        example_markdown: "",
1894        status: Status::Emerging,
1895        since: "0",
1896        description: "Syntax-highlight token: type name.",
1897    },
1898    ComponentEntry {
1899        class: "moss-hl-number",
1900        kind: "instance",
1901        parent: "",
1902        data_attrs: &[],
1903        example_html: r#"<span class="moss-hl-number">42</span>"#,
1904        example_markdown: "",
1905        status: Status::Emerging,
1906        since: "0",
1907        description: "Syntax-highlight token: numeric literal.",
1908    },
1909    ComponentEntry {
1910        class: "moss-hl-operator",
1911        kind: "instance",
1912        parent: "",
1913        data_attrs: &[],
1914        example_html: r#"<span class="moss-hl-operator">+</span>"#,
1915        example_markdown: "",
1916        status: Status::Emerging,
1917        since: "0",
1918        description: "Syntax-highlight token: operator.",
1919    },
1920    ComponentEntry {
1921        class: "moss-hl-builtin",
1922        kind: "instance",
1923        parent: "",
1924        data_attrs: &[],
1925        example_html: r#"<span class="moss-hl-builtin">print</span>"#,
1926        example_markdown: "",
1927        status: Status::Emerging,
1928        since: "0",
1929        description: "Syntax-highlight token: builtin identifier.",
1930    },
1931    ComponentEntry {
1932        class: "moss-hl-tag",
1933        kind: "instance",
1934        parent: "",
1935        data_attrs: &[],
1936        example_html: r#"<span class="moss-hl-tag">div</span>"#,
1937        example_markdown: "",
1938        status: Status::Emerging,
1939        since: "0",
1940        description: "Syntax-highlight token: markup tag name.",
1941    },
1942    ComponentEntry {
1943        class: "moss-hl-attr",
1944        kind: "instance",
1945        parent: "",
1946        data_attrs: &[],
1947        example_html: r#"<span class="moss-hl-attr">class</span>"#,
1948        example_markdown: "",
1949        status: Status::Emerging,
1950        since: "0",
1951        description: "Syntax-highlight token: attribute name.",
1952    },
1953    ComponentEntry {
1954        class: "moss-hl-meta",
1955        kind: "instance",
1956        parent: "",
1957        data_attrs: &[],
1958        example_html: r#"<span class="moss-hl-meta">@derive</span>"#,
1959        example_markdown: "",
1960        status: Status::Emerging,
1961        since: "0",
1962        description: "Syntax-highlight token: meta/annotation.",
1963    },
1964    ComponentEntry {
1965        class: "moss-hl-addition-bg",
1966        kind: "instance",
1967        parent: "",
1968        data_attrs: &[],
1969        example_html: r#"<span class="moss-hl-addition-bg">+ added line</span>"#,
1970        example_markdown: "",
1971        status: Status::Emerging,
1972        since: "0",
1973        description: "Syntax-highlight diff token: added-line background.",
1974    },
1975    ComponentEntry {
1976        class: "moss-hl-deletion",
1977        kind: "instance",
1978        parent: "",
1979        data_attrs: &[],
1980        example_html: r#"<span class="moss-hl-deletion">- removed line</span>"#,
1981        example_markdown: "",
1982        status: Status::Emerging,
1983        since: "0",
1984        description: "Syntax-highlight diff token: removed-line text.",
1985    },
1986    ComponentEntry {
1987        class: "moss-hl-deletion-bg",
1988        kind: "instance",
1989        parent: "",
1990        data_attrs: &[],
1991        example_html: r#"<span class="moss-hl-deletion-bg">- removed line</span>"#,
1992        example_markdown: "",
1993        status: Status::Emerging,
1994        since: "0",
1995        description: "Syntax-highlight diff token: removed-line background.",
1996    },
1997    ComponentEntry {
1998        class: "moss-recent",
1999        kind: "container",
2000        parent: "",
2001        data_attrs: &[],
2002        example_html: r#"<ul class="moss-recent">
2003  <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>
2004</ul>"#,
2005        example_markdown: ":::recent {count=5 since=\"2026-01-01\"}\n:::\n",
2006        status: Status::Emerging,
2007        since: "0",
2008        description: "Auto-generated list of recent posts emitted by the `:::recent` shortcode. Sorted newest-first; date and description slots are filled per child. No default CSS in the bundled theme — theme authors style it freely.",
2009    },
2010    ComponentEntry {
2011        class: "moss-recent__date",
2012        kind: "instance",
2013        parent: "moss-recent",
2014        data_attrs: &[],
2015        example_html: r#"<div class="moss-recent__date">2026-04-12</div>"#,
2016        example_markdown: "",
2017        status: Status::Emerging,
2018        since: "0",
2019        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.",
2020    },
2021    ComponentEntry {
2022        class: "moss-recent__desc",
2023        kind: "instance",
2024        parent: "moss-recent",
2025        data_attrs: &[],
2026        example_html: r#"<div class="moss-recent__desc">A walk through the garden.</div>"#,
2027        example_markdown: "",
2028        status: Status::Emerging,
2029        since: "0",
2030        description: "Per-entry description slot inside `.moss-recent` (BEM child). Sourced from frontmatter `description`; empty when unset.",
2031    },
2032    // -------------------------------------------------------------------
2033    // Ambient loop video — JS-injected wrapper + toggle (§3.5).
2034    // The <video data-loop> synthesizer emits `data-loop` on the <video>;
2035    // ambient-video.ts wraps it at init time.
2036    // -------------------------------------------------------------------
2037    ComponentEntry {
2038        class: "moss-ambient-video",
2039        kind: "standalone",
2040        parent: "",
2041        data_attrs: &[
2042            DataAttr {
2043                name: "data-paused",
2044                values: &[],
2045                default: "",
2046                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.",
2047            },
2048        ],
2049        example_html: r#"<div class="moss-ambient-video">
2050  <video data-loop src="clip.mp4" autoplay muted loop playsinline preload="metadata"></video>
2051  <button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>
2052</div>"#,
2053        example_markdown: "![[clip.mp4|loop]]",
2054        status: Status::Emerging,
2055        since: "1",
2056        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.",
2057    },
2058    ComponentEntry {
2059        class: "moss-ambient-toggle",
2060        kind: "instance",
2061        parent: "moss-ambient-video",
2062        data_attrs: &[],
2063        example_html: r#"<button class="moss-ambient-toggle" type="button" aria-label="Pause video">⏸</button>"#,
2064        example_markdown: "",
2065        status: Status::Emerging,
2066        since: "1",
2067        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).",
2068    },
2069    // -------------------------------------------------------------------
2070    // LaTeX math (ADR-030). P1 emits the escaped source in a marked
2071    // `<code>`; P2 replaces the element's *contents* with a typeset
2072    // `<svg>` while keeping the class and `data-moss-math` stable, so a
2073    // theme selector written against P1 keeps working across the upgrade.
2074    // -------------------------------------------------------------------
2075    ComponentEntry {
2076        class: "moss-math",
2077        kind: "standalone",
2078        parent: "",
2079        data_attrs: &[
2080            DataAttr {
2081                name: "data-moss-math",
2082                values: &["inline", "display"],
2083                default: "inline",
2084                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.",
2085            },
2086        ],
2087        example_html: r#"<code class="moss-math" data-moss-math="inline">$E = mc^2$</code>"#,
2088        example_markdown: "Energy $E = mc^2$.",
2089        status: Status::Emerging,
2090        since: "1",
2091        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).",
2092    },
2093    ComponentEntry {
2094        class: "moss-math-scroll",
2095        kind: "container",
2096        parent: "",
2097        data_attrs: &[],
2098        example_html: r#"<div class="moss-math-scroll"><svg class="moss-math" data-moss-math="display">…</svg></div>"#,
2099        example_markdown: "$$E = mc^2$$",
2100        status: Status::Emerging,
2101        since: "1",
2102        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%`.",
2103    },
2104];
2105
2106/// Implementation classes that are emitted by moss for internal functionality
2107/// but must not appear in the public theme-facing contract (`moss describe` /
2108/// `docs/contract/reference.md`). These classes ARE present in `COMPONENTS` for
2109/// the sync-test to validate their HTML class literals, but `is_public()` hides
2110/// them from agents, themes, and `reference.md` generation.
2111const INTERNAL_CLASSES: &[&str] = &[
2112    "moss-apply",
2113    "moss-apply-form",
2114    "moss-apply-matters",
2115    "moss-apply-hp",
2116    "moss-apply-status",
2117    "moss-apply-helper",
2118];
2119
2120impl ComponentEntry {
2121    /// True for entries that belong in the public, agent/theme-facing surface.
2122    /// v1 rule: not retired AND not an internal implementation class.
2123    ///
2124    /// Internal classes (e.g. all `moss-apply*`) stay in COMPONENTS so the
2125    /// sync-test can validate them, but they must not surface in `moss describe`
2126    /// or `docs/contract/reference.md` — they are subject to change at any time.
2127    pub fn is_public(&self) -> bool {
2128        self.status != Status::Retired && !INTERNAL_CLASSES.contains(&self.class)
2129    }
2130}
2131
2132/// Iterator over class names with `Status::Retired`. Used by the build
2133/// pipeline's theme lint to warn users about pre-v1 vocabulary.
2134///
2135/// Exposed as an iterator over `&'static str` so callers don't need to
2136/// import the `Status` enum (keeps moss-core's surface narrow).
2137pub fn retired_class_names() -> impl Iterator<Item = &'static str> {
2138    COMPONENTS.iter()
2139        .filter(|e| e.status == Status::Retired)
2140        .map(|e| e.class)
2141}
2142
2143#[cfg(test)]
2144mod tests {
2145    use super::*;
2146
2147    /// Orphan-gate: every class in `INTERNAL_CLASSES` must exist as a `class`
2148    /// in `COMPONENTS`. If a class is renamed in the emitter *and* in
2149    /// `INTERNAL_CLASSES` but forgotten in `COMPONENTS`, it would silently
2150    /// re-enter the public contract surface (`is_public()` only hides known
2151    /// internals). This test prevents that gap.
2152    #[test]
2153    fn every_internal_class_has_a_components_entry() {
2154        let component_classes: std::collections::HashSet<&'static str> =
2155            COMPONENTS.iter().map(|e| e.class).collect();
2156        for &internal in INTERNAL_CLASSES {
2157            assert!(
2158                component_classes.contains(internal),
2159                "INTERNAL_CLASSES entry '{}' has no matching entry in COMPONENTS — \
2160                 add a ComponentEntry for it or remove it from INTERNAL_CLASSES",
2161                internal
2162            );
2163        }
2164    }
2165}