diff --git a/clients/web/README.md b/clients/web/README.md index 619865ab..35000995 100644 --- a/clients/web/README.md +++ b/clients/web/README.md @@ -70,12 +70,27 @@ merely handled unevenly — and selecting the tool renders a **Schema portability** section (`SchemaFindingsList`) above the argument form, one block per finding with its path, the problem, and a concrete fix. +That section **opens collapsed**, behind a count badge reading +`N error(s), M warning(s)` (`#2205`). The findings address the *server author* +but render in the panel the *caller* fills in, so on a server whose schemas are +broadly unportable they used to put the same wall of text ahead of every tool's +first input — +`test-servers/configs/unportable-schemas-many-http.json` is 26 findings over +four tools, none of which a caller needs in order to fill the form. Expanding it +is one click, and the choice is **global rather than per tool** +(`useSchemaFindingsExpanded`, stored as `inspector.schemaFindings.expanded`): +this panel is reused across selections, so a per-tool disclosure would +re-collapse on every click and reproduce the same scrolling. The badge stays +visible either way, so nothing about a tool's standing is hidden by the closed +state. + Both read [`core/json/schemaLint.ts`](../../core/json/schemaLint.ts), which is also what backs the TUI's detail pane and the CLI's `--strict` report — so the three clients cannot disagree about whether a schema is portable. That module's header explains why it is a portability lint rather than a JSON Schema validator. `test-servers/configs/unportable-schemas-http.json` is a server that -exercises every rule. +exercises every rule, and `unportable-schemas-many-http.json` the same rules at +volume. ## Non-component code: `src/lib` vs `src/utils` diff --git a/clients/web/src/components/elements/JsonEditor/JsonEditor.tsx b/clients/web/src/components/elements/JsonEditor/JsonEditor.tsx index 751c1ecf..22a0dc05 100644 --- a/clients/web/src/components/elements/JsonEditor/JsonEditor.tsx +++ b/clients/web/src/components/elements/JsonEditor/JsonEditor.tsx @@ -1,6 +1,6 @@ import { useEffect, useId, useRef } from "react"; import type { ReactNode } from "react"; -import { Input, useComputedColorScheme } from "@mantine/core"; +import { Input, Paper, useComputedColorScheme } from "@mantine/core"; import type { Ace } from "ace-builds"; import AceEditor from "react-ace"; import ace from "ace-builds/src-noconflict/ace"; @@ -16,6 +16,25 @@ import jsonWorkerUrl from "ace-builds/src-noconflict/worker-json.js?url"; // it must not be repeated per mount. ace.config.setModuleUrl("ace/mode/json_worker", jsonWorkerUrl); +/** + * The editor's frame. + * + * Ace paints its own background edge to edge with nothing around it, so an + * editor dropped into a panel reads as a discoloured patch rather than as a + * field — most visibly on the Tools tab, where "Edit as JSON" replaces a column + * of bordered inputs with one borderless slab. The border gives it the same + * edge every Mantine input beside it has. + * + * `variant="contained"` supplies the `overflow: hidden` that makes the rounded + * corners actually clip Ace's square background; without it the corners are + * painted over and the radius is invisible. + */ +const EditorFrame = Paper.withProps({ + variant: "contained", + withBorder: true, + radius: "sm", +}); + export interface JsonEditorProps { /** * The text the editor displays. This is a **text** contract, not a JSON one: @@ -228,73 +247,75 @@ export function JsonEditor({ opacity={disabled ? 0.6 : undefined} w="100%" > - ` points at. - name={`${wrapperId}-editor`} - // A class, not a style: Ace renders its own DOM and paints a caret even - // when read-only, which reads as an editable field whose keystrokes are - // being swallowed. Hiding it needs a selector into that DOM, which is - // the same reason the gutter override in App.css exists. - // - // `""` rather than `undefined` for the off case: react-ace's - // `componentDidUpdate` reads `prevProps.className.trim()` whenever the - // class changes, with no guard — so going *to* a class from `undefined` - // throws, and going *from* one to `undefined` writes the literal class - // name "undefined" onto the element. Both are reachable here, because - // read-only is derived state: a tool form disables itself while a call - // is in flight, which flips this on an editor already mounted. - className={isReadOnly ? "json-editor-readonly" : ""} - value={value} - onChange={handleChange} - readOnly={isReadOnly} - width="100%" - minLines={minLines} - maxLines={maxLines} - tabSize={2} - showPrintMargin={false} - // A read-only editor is a rendering of someone else's payload, so it - // carries none of the caret furniture an editable one does. - highlightActiveLine={!isReadOnly} - editorProps={{ $blockScrolling: Infinity }} - onLoad={(editor) => { - editorRef.current = editor; - // `textInputAriaLabel` alone is not enough: Ace composes the hidden - // textarea's label as "