Skip to content

Commit f84c74e

Browse files
dmealingclaude
andauthored
feat(docs-site): render remaining authored attrs (relationships, origins, sources, layout, object/field/template) (#178)
Extends the view.* attr rendering (0.15.12) to every remaining authored `@attr`, so a model's full metadata surfaces in the docs and coverage reports zero "not rendered by any page" warnings on a real ~280-page model: - object pages: relationship @onDelete/@onUpdate; origin @of/@agg/@filter on the field-provenance table; a view source's @view in the storage line; a grid @layout section (@pageSize/@defaultSortOrder…); object-level @attrs (e.g. @dataflow/@neo4j) in an Attributes section; field-level @attrs (@column/@storage…) as badges; identity @constraintName in the index detail cell. - template pages: any non-standard authored attr on prompt + output (e.g. @dataflow). A generic `otherAttrs(node, skip)` catch-all consumes + renders whatever a bespoke renderer doesn't, so a consumer's own vocabulary is documented from a bare registration and future attrs never silently go un-rendered. The acme fixture gains an example of each; golden regenerated (deterministic; all builders escape at the boundary since this render lib does not). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dbf6555 commit f84c74e

25 files changed

Lines changed: 172 additions & 35 deletions

server/typescript/packages/docs-site/src/builders/object-data.ts

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface FieldRow { name: string; type: string; isArray: boolean; requir
3535

3636
/** Render an attr value for display: an object (e.g. view.badge @variantMap) as
3737
* ordered `k: v` pairs, an array as a comma list, else the scalar. */
38-
function fmtAttrValue(v: unknown): string {
38+
export function fmtAttrValue(v: unknown): string {
3939
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
4040
return Object.entries(v as Record<string, unknown>)
4141
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
@@ -45,15 +45,45 @@ function fmtAttrValue(v: unknown): string {
4545
if (Array.isArray(v)) return v.map(String).join(", ");
4646
return String(v);
4747
}
48+
49+
/** A node's authored attrs not already rendered by a specific renderer, as
50+
* escaped key/value pairs — consumed so they don't surface as "not rendered by
51+
* any page" in coverage. `skip` names the attrs a specific renderer handles.
52+
* Deterministic (sorted by name). This is the generic catch-all that documents
53+
* a consumer's own attrs (e.g. object.@dataflow) + the structural ones a
54+
* bespoke renderer doesn't (e.g. relationship.@onDelete). */
55+
function otherAttrs(node: MetaData, cov: CoverageTracker, skip: Set<string>): ViewAttr[] {
56+
return [...node.ownAttrs()]
57+
.filter(([n]) => !skip.has(n))
58+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
59+
.map(([n, v]) => { cov.consumeAttr(node, n); return { name: esc(n), value: esc(fmtAttrValue(v)) }; });
60+
}
61+
62+
/** Consume a node + its whole subtree (nodes + attrs) so a section that renders
63+
* only a summary (e.g. a grid layout's top-level attrs) doesn't leave its nested
64+
* config flagged as un-rendered. */
65+
function consumeSubtree(node: MetaData, cov: CoverageTracker): void {
66+
cov.consumeNode(node);
67+
for (const [n] of node.ownAttrs()) cov.consumeAttr(node, n);
68+
for (const c of node.ownChildren()) consumeSubtree(c, cov);
69+
}
70+
4871
export interface IndexRow { name: string; kind: string; fields: string; extra: string; unique: boolean; }
4972
export interface ValidatorRow { scope: "field" | "object"; subject: string; rule: string; // human-readable, HTML-escaped
5073
}
51-
export interface RelationRow { name: string; toName: string; toHref: string; cardinality: string; }
52-
export interface OriginRow { field: string; from: string; via: string; }
74+
export interface RelationRow { name: string; toName: string; toHref: string; cardinality: string; attrs: ViewAttr[]; }
75+
export interface OriginRow { field: string; from: string; via: string; attrs: ViewAttr[]; }
76+
export interface LayoutRow { kind: string; attrs: ViewAttr[]; }
77+
78+
// Field attrs fieldRow renders specifically (as their own badges / enum / ref);
79+
// everything else authored on a field (e.g. @column, @storage) is rendered
80+
// generically as a badge so it isn't flagged "not rendered" in coverage.
81+
const FIELD_KNOWN_ATTRS = new Set(["required", "deprecated", "maxLength", "dbColumnType", "default", "xmlText", "values", "objectRef", "description"]);
5382
export interface HierRow { name: string; href: string; level: number; self: boolean; }
5483
export interface ObjectPageData {
5584
name: string; kindBadge: string; isAbstract: boolean; isView: boolean; generation: string;
5685
pkg: string; href: string; breadcrumbHtml: string; desc: string; tableName?: string | undefined; pkHtml?: string | undefined;
86+
objectAttrs: ViewAttr[]; storageAttrs: ViewAttr[]; layouts: LayoutRow[];
5787
ownFields: FieldRow[]; inheritedFields: FieldRow[]; indexes: IndexRow[]; validators: ValidatorRow[];
5888
relations: RelationRow[]; origins: OriginRow[]; hierarchy: HierRow[]; inheritanceMermaid?: string | undefined;
5989
neighborhoodMermaid?: string | undefined; neighborhoodLegend?: { pkg: string; fill: string; stroke: string }[] | undefined; neighborhoodMore?: number | undefined;
@@ -115,6 +145,12 @@ function fieldRow(f: MetaData, ownerHref: string, g: LinkGraph, cov: CoverageTra
115145
.map(([an, av]) => { cov.consumeAttr(v, an); return { name: esc(an), value: esc(fmtAttrValue(av)) }; });
116146
views.push({ subType: esc(v.subType), attrs: vAttrs });
117147
}
148+
// any other authored field attrs (e.g. @column DB name, @storage) as badges
149+
for (const [n, val] of [...f.ownAttrs()].sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0))) {
150+
if (FIELD_KNOWN_ATTRS.has(n)) continue;
151+
cov.consumeAttr(f, n);
152+
bits.push(badge({ text: `@${n}=${fmtAttrValue(val)}`, cls: "badge-soft badge-ghost" }));
153+
}
118154
const desc = esc(a("description") ?? "");
119155
return { name: f.name, type: f.subType, isArray: f.resolvedIsArray(), required, badgesHtml: bits.join(" "), desc, enumValues, views, refHref, refName, inheritedFrom: undefined, anchor: `f-${f.name}` };
120156
}
@@ -136,6 +172,8 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
136172
const o = dn.node;
137173
cov.consumeNode(o);
138174
cov.consumeAttr(o, "description");
175+
// consumer/structural attrs authored on the object itself (e.g. @dataflow, @neo4j)
176+
const objectAttrs = otherAttrs(o, cov, new Set(["description"]));
139177

140178
// inheritance hierarchy rows (ancestors nearest-last so level increases downward) + self + direct children
141179
const anc = g.ancestors(fqn); // nearest-first
@@ -150,27 +188,32 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
150188

151189
// storage: table vs view, generation, pk
152190
let tableName: string | undefined, isView = false, pkHtml: string | undefined, generation = "";
191+
const storageAttrs: ViewAttr[] = [];
153192
for (const s of o.childrenOfType("source")) {
154193
cov.consumeNode(s);
155194
const t = s.attr("table"); if (t !== undefined) { cov.consumeAttr(s, "table"); tableName = String(t); }
156195
const kind = s.attr("kind"); if (kind !== undefined) { cov.consumeAttr(s, "kind"); if (String(kind) === "view") isView = true; }
196+
// extra source attrs — e.g. a view source's @view (view name) — rendered in the storage section
197+
storageAttrs.push(...otherAttrs(s, cov, new Set(["table", "kind"])));
157198
}
158199
// indexes: identity (pk/secondary) + index.lookup with tuning detail
159200
const indexes: IndexRow[] = [];
160201
for (const id of o.childrenOfType("identity")) {
161202
cov.consumeNode(id);
162203
const flds = id.attr("fields"); if (flds !== undefined) cov.consumeAttr(id, "fields");
163204
const fields = Array.isArray(flds) ? flds.join(", ") : String(flds ?? "");
205+
// other authored identity attrs (e.g. @constraintName) rendered into the detail cell
206+
const idExtra = otherAttrs(id, cov, new Set(["fields", "references", "enforce", "generation"])).map((x) => `@${x.name}=${x.value}`).join(" · ");
164207
if (id.subType === "primary") {
165208
pkHtml = `<code>${esc(fields)}</code>`;
166209
const gen = id.attr("generation"); if (gen !== undefined) { cov.consumeAttr(id, "generation"); generation = String(gen); }
167-
indexes.push({ name: id.name, kind: "primary", fields, extra: "", unique: true });
210+
indexes.push({ name: id.name, kind: "primary", fields, extra: idExtra, unique: true });
168211
} else if (id.subType === "secondary") {
169-
indexes.push({ name: id.name, kind: "unique", fields, extra: "", unique: true });
212+
indexes.push({ name: id.name, kind: "unique", fields, extra: idExtra, unique: true });
170213
} else if (id.subType === "reference") {
171214
const ref = id.attr("references"); if (ref !== undefined) cov.consumeAttr(id, "references");
172215
const enf = id.attr("enforce"); if (enf !== undefined) cov.consumeAttr(id, "enforce");
173-
indexes.push({ name: id.name, kind: "fk", fields, extra: [ref ? `→ ${esc(String(ref))}` : "", enf === false || enf === "false" ? "logical" : ""].filter(Boolean).join(" · "), unique: false });
216+
indexes.push({ name: id.name, kind: "fk", fields, extra: [ref ? `→ ${esc(String(ref))}` : "", enf === false || enf === "false" ? "logical" : "", idExtra].filter(Boolean).join(" · "), unique: false });
174217
}
175218
}
176219
for (const ix of o.childrenOfType("index")) {
@@ -201,17 +244,38 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
201244
}
202245
validators.sort((a, b) => a.subject.localeCompare(b.subject));
203246

204-
// relationships
247+
// relationships — bespoke name/target/cardinality + generic extras (@onDelete/@onUpdate/…)
248+
const relAttrs = new Map<string, ViewAttr[]>();
249+
for (const rel of o.childrenOfType("relationship")) {
250+
cov.consumeNode(rel); cov.consumeAttr(rel, "objectRef"); cov.consumeAttr(rel, "cardinality");
251+
relAttrs.set(rel.name, otherAttrs(rel, cov, new Set(["objectRef", "cardinality", "through", "sourceRefField", "symmetric"])));
252+
}
205253
const relations: RelationRow[] = g.relationshipsOf(fqn).map((r) => {
206254
const t = g.byFqn(r.toFqn);
207-
return { name: r.name, toName: t?.name ?? r.toFqn, toHref: t ? g.relHref(dn.href, t.href) : "", cardinality: r.cardinality };
255+
return { name: r.name, toName: t?.name ?? r.toFqn, toHref: t ? g.relHref(dn.href, t.href) : "", cardinality: r.cardinality, attrs: relAttrs.get(r.name) ?? [] };
208256
});
209-
for (const rel of o.childrenOfType("relationship")) { cov.consumeNode(rel); cov.consumeAttr(rel, "objectRef"); cov.consumeAttr(rel, "cardinality"); }
210257

211-
// origin provenance
212-
const origins: OriginRow[] = g.originsOf(fqn).map((r) => ({ field: r.field, from: esc(r.from), via: esc(r.via) }))
258+
// origin provenance — bespoke field/from/via + generic extras (@of/@agg/@filter/…)
259+
const originAttrs = new Map<string, ViewAttr[]>();
260+
for (const f of o.childrenOfType("field")) for (const org of f.childrenOfType("origin")) {
261+
cov.consumeNode(org); cov.consumeAttr(org, "from"); cov.consumeAttr(org, "via");
262+
const cur = originAttrs.get(f.name) ?? [];
263+
cur.push(...otherAttrs(org, cov, new Set(["from", "via"])));
264+
originAttrs.set(f.name, cur);
265+
}
266+
const origins: OriginRow[] = g.originsOf(fqn).map((r) => ({ field: r.field, from: esc(r.from), via: esc(r.via), attrs: originAttrs.get(r.field) ?? [] }))
213267
.sort((a, b) => a.field.localeCompare(b.field));
214-
for (const f of o.childrenOfType("field")) for (const org of f.childrenOfType("origin")) { cov.consumeNode(org); cov.consumeAttr(org, "from"); cov.consumeAttr(org, "via"); }
268+
269+
// grid/form layout (admin-ui presentation on projections): render the layout node's
270+
// own attrs (@pageSize/@defaultSortOrder/…); consume nested column config so it
271+
// isn't flagged un-rendered.
272+
const layouts: LayoutRow[] = [];
273+
for (const l of o.childrenOfType("layout")) {
274+
cov.consumeNode(l);
275+
const attrs = otherAttrs(l, cov, new Set());
276+
for (const c of l.ownChildren()) consumeSubtree(c, cov);
277+
layouts.push({ kind: esc(l.subType), attrs });
278+
}
215279

216280
// fields own vs inherited
217281
const ownNames = new Set(o.ownChildren().filter((c) => c.type === "field").map((c) => c.name));
@@ -282,7 +346,7 @@ export function buildObjectPage(fqn: string, g: LinkGraph, cov: CoverageTracker)
282346
return {
283347
name: dn.name, kindBadge: o.subType, isAbstract: o.isAbstract, isView, generation,
284348
pkg: dn.pkg, href: dn.href, breadcrumbHtml: crumbs.join(" / "), desc: esc(o.attr("description") ?? ""),
285-
tableName, pkHtml, ownFields, inheritedFields, indexes, validators, relations, origins,
349+
tableName, pkHtml, objectAttrs, storageAttrs, layouts, ownFields, inheritedFields, indexes, validators, relations, origins,
286350
hierarchy, inheritanceMermaid, neighborhoodMermaid, neighborhoodLegend, neighborhoodMore, referencedBy, references, usedByTemplates, sourceFile: src,
287351
};
288352
}

server/typescript/packages/docs-site/src/builders/output-data.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,25 @@ import { LinkGraph } from "../link-graph.js";
44
import type { CoverageTracker } from "../coverage.js";
55
import type { CommentDocs } from "../yaml-comments.js";
66
import { esc } from "../badges.js";
7+
import { fmtAttrValue } from "./object-data.js";
78

8-
export interface OutputPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; format: string; kind: string; textRef: string; textRefResolves: boolean; payloadName: string; payloadHref: string; desc: string; fields: { name: string; type: string; isArray: boolean; wire: string; note: string; refHtml: string }[]; }
9+
// Rendered specifically (own fields/badges); everything else a template authors
10+
// (e.g. a consumer's @dataflow) is rendered generically as extraAttrsHtml.
11+
const OUTPUT_KNOWN_ATTRS = new Set(["payloadRef", "textRef", "format", "kind", "description"]);
12+
13+
export interface OutputPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; format: string; kind: string; textRef: string; textRefResolves: boolean; payloadName: string; payloadHref: string; desc: string; extraAttrsHtml: string; fields: { name: string; type: string; isArray: boolean; wire: string; note: string; refHtml: string }[]; }
914

1015
export function buildOutputPage(fqn: string, g: LinkGraph, cov: CoverageTracker, docs: CommentDocs, sourceDirs: string[] = []): OutputPageData {
1116
const dn = g.byFqn(fqn)!;
1217
const t = dn.node;
13-
cov.consumeNode(t); cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef"); cov.consumeAttr(t, "format");
18+
cov.consumeNode(t); cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef"); cov.consumeAttr(t, "format"); cov.consumeAttr(t, "kind");
1419
const format = String(t.attr("format") ?? "text");
20+
// any other authored template attrs (e.g. @maxChars, @requiredTags, a consumer's @dataflow)
21+
const extraAttrsHtml = [...t.ownAttrs()]
22+
.filter(([n]) => !OUTPUT_KNOWN_ATTRS.has(n))
23+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
24+
.map(([n, v]) => { cov.consumeAttr(t, n); return `<span class="badge badge-ghost badge-sm font-mono">@${esc(n)}=${esc(fmtAttrValue(v))}</span>`; })
25+
.join(" ");
1526
const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload");
1627
const payload = pRef ? g.byFqn(pRef.to) : undefined;
1728
const fields = (payload?.node.childrenOfType("field") ?? []).map((f) => {
@@ -29,7 +40,7 @@ export function buildOutputPage(fqn: string, g: LinkGraph, cov: CoverageTracker,
2940
const textRefResolves = sourceDirs.some((d) => existsSync(join(d, ...textRef.split("/")) + ".mustache"));
3041
return { name: dn.name, pkg: dn.pkg, href: dn.href,
3142
breadcrumbHtml: `<a href="${esc(g.relHref(dn.href, "index.html"))}">index</a> / <a href="${esc("index.html")}">${esc(dn.pkg)}</a> / ${esc(dn.name)}`,
32-
format, kind: String(t.attr("kind") ?? "document"), textRef, textRefResolves,
43+
format, kind: String(t.attr("kind") ?? "document"), textRef, textRefResolves, extraAttrsHtml,
3344
payloadName: payload?.name ?? "", payloadHref: payload ? esc(g.relHref(dn.href, payload.href)) : "",
3445
desc: payload ? esc(payload.node.attr("description") ?? docs.objectDesc.get(payload.name) ?? "") : "", fields };
3546
}

server/typescript/packages/docs-site/src/builders/prompt-data.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { LinkGraph } from "../link-graph.js";
44
import type { CoverageTracker } from "../coverage.js";
55
import { highlightMustache } from "../mustache-highlight.js";
66
import { esc } from "../badges.js";
7+
import { fmtAttrValue } from "./object-data.js";
8+
9+
// Template attrs rendered specifically (as their own badges) — everything else a
10+
// template authors (e.g. a consumer's @dataflow) is rendered generically below.
11+
const PROMPT_KNOWN_ATTRS = new Set(["format", "maxTokens", "requiredSlots", "model", "responseRef", "maxChars", "promptStyle", "payloadRef", "textRef", "description"]);
712

813
export interface PayloadTreeRow { indent: number; name: string; type: string; isArray: boolean; anchor: string; desc: string; refHtml: string; }
914
export interface PromptPageData { name: string; pkg: string; href: string; breadcrumbHtml: string; attrsHtml: string; desc: string; payloadName: string; payloadHref: string; payloadTree: PayloadTreeRow[]; sourceHtml?: string | undefined; sourceMissingNote?: string | undefined; tocHtml?: string | undefined; packageFiles: { file: string; html: string }[]; }
@@ -17,6 +22,12 @@ export function buildPromptPage(fqn: string, g: LinkGraph, cov: CoverageTracker,
1722
const v = t.attr(a); if (v !== undefined) { cov.consumeAttr(t, a); attrs.push(`<span class="badge badge-ghost badge-sm">@${esc(a)} ${esc(v)}</span>`); }
1823
}
1924
cov.consumeAttr(t, "payloadRef"); cov.consumeAttr(t, "textRef");
25+
// any other authored attrs (e.g. a consumer's @dataflow) rendered generically
26+
for (const [n, v] of [...t.ownAttrs()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) {
27+
if (PROMPT_KNOWN_ATTRS.has(n)) continue;
28+
cov.consumeAttr(t, n);
29+
attrs.push(`<span class="badge badge-ghost badge-sm font-mono">@${esc(n)}=${esc(fmtAttrValue(v))}</span>`);
30+
}
2031
// payload tree (root + one nested level)
2132
const pRef = g.refsFrom(fqn).find((r) => r.kind === "payload");
2233
const payload = pRef ? g.byFqn(pRef.to) : undefined;

server/typescript/packages/docs-site/templates/object.html.mustache

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
{{#isAbstract}}<span class="badge badge-ghost badge-sm align-middle">abstract</span>{{/isAbstract}}</h1>
66
<div class="font-mono text-xs opacity-60 mt-1">
77
{{#tableName}}{{#isView}}view{{/isView}}{{^isView}}table{{/isView}} <code>{{tableName}}</code>{{/tableName}}
8-
{{#pkHtml}} · pk {{{pkHtml}}}{{/pkHtml}}{{#generation}} · gen {{generation}}{{/generation}}</div>
8+
{{#pkHtml}} · pk {{{pkHtml}}}{{/pkHtml}}{{#generation}} · gen {{generation}}{{/generation}}{{#storageAttrs}} · {{name}} <code>{{value}}</code>{{/storageAttrs}}</div>
99
{{#desc}}<section id="s-overview" class="mt-4"><p class="text-sm leading-relaxed">{{desc}}</p></section>{{/desc}}
10+
{{#objectAttrs.length}}<section id="s-attributes" class="mt-4 text-xs"><span class="opacity-50 mr-1">attributes</span>{{#objectAttrs}}<span class="badge badge-soft badge-ghost badge-xs mr-1 font-mono">@{{name}}={{value}}</span>{{/objectAttrs}}</section>{{/objectAttrs.length}}
1011

1112
<section id="s-fields" class="mt-6"><h2 class="text-lg font-semibold">Fields</h2>
1213
<div class="overflow-x-auto"><table class="table table-xs"><thead><tr><th>Field</th><th>Type</th><th></th><th>Description</th></tr></thead><tbody>
@@ -28,10 +29,13 @@
2829
<table class="table table-xs"><tbody>{{#validators}}<tr><td class="font-mono text-xs opacity-70">{{subject}}</td><td class="font-mono text-xs">{{rule}}</td></tr>{{/validators}}</tbody></table></section>{{/validators.length}}
2930

3031
{{#relations.length}}<section id="s-relationships" class="mt-6"><h2 class="text-lg font-semibold">Relationships</h2>
31-
<table class="table table-xs"><tbody>{{#relations}}<tr><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{cardinality}}</td><td><a class="link font-mono text-xs" href="{{toHref}}">{{toName}}</a></td></tr>{{/relations}}</tbody></table></section>{{/relations.length}}
32+
<table class="table table-xs"><tbody>{{#relations}}<tr><td class="font-mono">{{name}}</td><td class="text-xs opacity-60">{{cardinality}}</td><td><a class="link font-mono text-xs" href="{{toHref}}">{{toName}}</a></td><td class="text-xs opacity-60">{{#attrs}}<span class="font-mono mr-2">@{{name}}={{value}}</span>{{/attrs}}</td></tr>{{/relations}}</tbody></table></section>{{/relations.length}}
3233

3334
{{#origins.length}}<section id="s-provenance" class="mt-6"><h2 class="text-lg font-semibold">Field provenance</h2>
34-
<table class="table table-xs"><thead><tr><th>Field</th><th>From</th><th>Via</th></tr></thead><tbody>{{#origins}}<tr><td class="font-mono">{{field}}</td><td class="font-mono text-xs opacity-70">{{from}}</td><td class="font-mono text-xs opacity-70">{{via}}</td></tr>{{/origins}}</tbody></table></section>{{/origins.length}}
35+
<table class="table table-xs"><thead><tr><th>Field</th><th>From</th><th>Via</th><th></th></tr></thead><tbody>{{#origins}}<tr><td class="font-mono">{{field}}</td><td class="font-mono text-xs opacity-70">{{from}}</td><td class="font-mono text-xs opacity-70">{{via}}</td><td class="text-xs opacity-60">{{#attrs}}<span class="font-mono mr-2">@{{name}}={{value}}</span>{{/attrs}}</td></tr>{{/origins}}</tbody></table></section>{{/origins.length}}
36+
37+
{{#layouts.length}}<section id="s-layout" class="mt-6"><h2 class="text-lg font-semibold">Layout</h2>
38+
<table class="table table-xs"><tbody>{{#layouts}}<tr><td class="font-mono text-xs opacity-70">{{kind}}</td><td class="text-xs">{{#attrs}}<span class="font-mono opacity-60 mr-2">@{{name}}={{value}}</span>{{/attrs}}</td></tr>{{/layouts}}</tbody></table></section>{{/layouts.length}}
3539

3640
{{#inheritanceMermaid}}<section id="s-inheritance" class="mt-6"><h2 class="text-lg font-semibold">Inheritance</h2>
3741
<div class="card bg-base-100 border border-base-300 p-2 pl-diagram"><pre class="mermaid">{{inheritanceMermaid}}</pre></div></section>{{/inheritanceMermaid}}

0 commit comments

Comments
 (0)