Injectable React editor that is side-loaded onto the statically rendered page and edits through the CMS HTTP API (POST /api/edit). - Shadow-root UI over server HTML; patches morphed in, undo/redo from server inverses (reuses @k6n/cms-editor EditClient/morph/markdown editor) - Sign-in via go-auth device grant as the CMS client (aud matches), token refresh + 401 retry; live preview swapped in on load and on tab focus - Schema-driven property form (arrays, maps, variants, $ref, validation) - Every string property editable where it is rendered (value-bound inline fields and attribute chips), markdown WYSIWYG for marked fields - Block palette + drag & drop with ghost drop targets, insertion slots, sort snapping between siblings (Alt to nest) - x-style annotations: on-page padding/margin/gap handles, Style panel, wrap-in-layout; x-graph label/group for titles and grouping - Dev workflow: Vite proxy to the live site (token injected server-side) or an offline in-memory mock CMS; jsdom test suite Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
34 lines
1.7 KiB
TypeScript
34 lines
1.7 KiB
TypeScript
import type { JSONSchema } from '../api'
|
|
import { useForm } from './context'
|
|
import { Widget } from './Form'
|
|
import { defaultFor, discriminators, pickVariant, resolveSchema, variantLabel, variantsOf } from './resolve'
|
|
|
|
/** oneOf / anyOf: choose a branch, then edit it with that branch's own form. */
|
|
export function VariantField({ schema: s, value, onChange, path }: { schema: JSONSchema; value: unknown; onChange(v: unknown): void; path: string }) {
|
|
const { root } = useForm()
|
|
const branches = variantsOf(s) ?? []
|
|
const current = pickVariant(root, branches, value)
|
|
const choose = (i: number) => {
|
|
if (i < 0) return onChange(undefined)
|
|
const b = branches[i]
|
|
const base = defaultFor(root, b)
|
|
const consts = discriminators(root, b)
|
|
// Keep what the old branch and the new one share, so switching does not wipe the user's input.
|
|
const carried = value && typeof value === 'object' && base && typeof base === 'object' ? Object.fromEntries(Object.entries(value as object).filter(([k]) => k in (resolveSchema(root, b).schema.properties ?? {}))) : {}
|
|
onChange(base && typeof base === 'object' && !Array.isArray(base) ? { ...base, ...carried, ...consts } : base)
|
|
}
|
|
return (
|
|
<div className="cms-variant">
|
|
<select className="cms-input" aria-label="Variant" value={current} onChange={(e) => choose(Number(e.target.value))}>
|
|
<option value={-1}>Choose…</option>
|
|
{branches.map((b, i) => (
|
|
<option key={i} value={i}>
|
|
{variantLabel(root, b, i)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
{current >= 0 && <Widget schema={{ ...branches[current] }} value={value} onChange={onChange} path={path} required bare />}
|
|
</div>
|
|
)
|
|
}
|