From a82d3c338ea49184d170f2747c0ff0a140da5b89 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 00:25:00 +0000 Subject: [PATCH] web: fix white-screen on classes with non-empty superclasses Inside schema files, document_class.superclasses is an array of {class_name: "..."} objects, not plain strings (the index.json normalizes them to strings, hence the mismatch). Detail.tsx was rendering each ref directly as a React child, throwing "Objects are not valid as a React child" and unmounting the whole app on any class that wasn't a root. - Introduce SuperclassRef = string | {class_name, ...} and a superclassName() helper; use it when rendering superclass links. - Wrap Detail in an ErrorBoundary keyed on selection so a future render bug surfaces a message instead of whiting out the page. - Drop the leftover `main { max-width: 48rem; margin: 0 auto }` rule from index.css that was constraining the right pane and leaving large empty margins around the content. --- web/src/App.tsx | 5 ++++- web/src/Detail.tsx | 20 +++++++++++-------- web/src/ErrorBoundary.tsx | 42 +++++++++++++++++++++++++++++++++++++++ web/src/index.css | 6 ------ web/src/types.ts | 12 ++++++++++- 5 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 web/src/ErrorBoundary.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index 4a0bd99..1f8632c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -3,6 +3,7 @@ import type { SchemaIndex } from "./types"; import { buildTree, loadIndex, sortedFlat } from "./schemaIndex"; import { FlatList, Tree } from "./Tree"; import { Detail } from "./Detail"; +import { ErrorBoundary } from "./ErrorBoundary"; import "./styles.css"; type ViewMode = "tree" | "flat"; @@ -95,7 +96,9 @@ export default function App() {
{selectedEntry ? ( - + + + ) : (

Select a schema from the left to view its definition.

diff --git a/web/src/Detail.tsx b/web/src/Detail.tsx index 48b9513..19d4ac7 100644 --- a/web/src/Detail.tsx +++ b/web/src/Detail.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import type { IndexEntry, FieldDef, SchemaDocument } from "./types"; +import { superclassName } from "./types"; import { loadSchema } from "./schemaIndex"; interface Props { @@ -61,14 +62,17 @@ export function Detail({ entry, onSelect }: Props) {
Superclasses
{dc.superclasses && dc.superclasses.length > 0 ? ( - dc.superclasses.map((s, i) => ( - - {i > 0 && ", "} - - - )) + dc.superclasses.map((ref, i) => { + const name = superclassName(ref); + return ( + + {i > 0 && ", "} + + + ); + }) ) : ( none )} diff --git a/web/src/ErrorBoundary.tsx b/web/src/ErrorBoundary.tsx new file mode 100644 index 0000000..73cd785 --- /dev/null +++ b/web/src/ErrorBoundary.tsx @@ -0,0 +1,42 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; + +interface Props { + children: ReactNode; + // Reset the error state whenever this key changes (e.g. on selection change). + resetKey?: string | null; +} + +interface State { + error: Error | null; +} + +export class ErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidUpdate(prev: Props) { + if (prev.resetKey !== this.props.resetKey && this.state.error) { + this.setState({ error: null }); + } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error("Render error:", error, info.componentStack); + } + + render() { + if (this.state.error) { + return ( +
+

Could not render this schema

+
{this.state.error.message}
+

Try selecting a different schema, or view the raw JSON in the source file.

+
+ ); + } + return this.props.children; + } +} diff --git a/web/src/index.css b/web/src/index.css index d5c3613..155d5db 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -7,9 +7,3 @@ body { margin: 0; } - -main { - max-width: 48rem; - margin: 0 auto; - padding: 2rem 1rem; -} diff --git a/web/src/types.ts b/web/src/types.ts index 33a43d7..e83134b 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -49,7 +49,7 @@ export interface SchemaDocument { document_class?: { class_name: string; class_version?: string; - superclasses?: string[]; + superclasses?: SuperclassRef[]; maturity_level?: Maturity; }; depends_on?: DependsOnEntry[]; @@ -58,3 +58,13 @@ export interface SchemaDocument { // Meta-schemas and registries may have arbitrary other shapes; keep open. [key: string]: unknown; } + +// Inside a schema file, `superclasses` is an array of objects with at least +// a `class_name` key. The repo-level index.json normalizes these to plain +// strings, so callers may see either shape -- always pass through +// `superclassName()` to extract the string. +export type SuperclassRef = string | { class_name: string; [k: string]: unknown }; + +export function superclassName(ref: SuperclassRef): string { + return typeof ref === "string" ? ref : ref.class_name; +}