diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dbc591e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Normalize line endings to LF for all text files on every platform. +# Prevents Windows autocrlf checkouts (CRLF) from failing `biome check` +# (biome.json enforces lineEnding: "lf"). +* text=auto eol=lf diff --git a/README.md b/README.md index 49cff08..e7d2782 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,81 @@ const customTranslation: Translation = { Use [`src/i18n/locales/en.ts`](https://github.com/lovasoa/jsonjoy-builder/blob/main/src/i18n/locales/en.ts) as the reference for all available keys. +## Plugin Registry + +The `registry` prop lets you replace any UI component or layout slot with your own design-system adapter. It works on ``, ``, and ``. + +### Replace leaf components + +```tsx +import { SchemaBuilder } from "jsonjoy-builder"; + + +``` + +### Replace layout slots + +```tsx +registry={{ + slots: { + // Layout wrappers that receive children and visual metadata + FieldFrame: MyFieldFrame, + FieldHeader: MyFieldHeader, + FieldMain: MyFieldMain, + FieldActions: MyFieldActions, + FieldBody: MyFieldBody, + }, + slotProps: { + // Extra props forwarded to each slot component + FieldFrame: { variant: "compact" }, + }, +}} +``` + +### Integration with a design system + +```tsx +// Wrap in your own ThemeProvider, pass adapters via registry +import { ThemeProvider, Button, TextField, Switch } from "./design-system"; + +const adapters = { + Button: (props) => + ))} + + {page === "default" ? : } + + ); +}; export default App; diff --git a/demo/pages/MuiRegistry.tsx b/demo/pages/MuiRegistry.tsx new file mode 100644 index 0000000..d69bc5d --- /dev/null +++ b/demo/pages/MuiRegistry.tsx @@ -0,0 +1,343 @@ +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Chip from "@mui/material/Chip"; +import CssBaseline from "@mui/material/CssBaseline"; +import FormLabel from "@mui/material/FormLabel"; +import Paper from "@mui/material/Paper"; +import Switch from "@mui/material/Switch"; +import { createTheme, ThemeProvider } from "@mui/material/styles"; +import TextField from "@mui/material/TextField"; +import ToggleButton from "@mui/material/ToggleButton"; +import { useState } from "react"; +import { + type JsonSchema, + SchemaBuilderRegistryProvider, + SchemaFieldsEditor, +} from "../../src/index.ts"; + +// ── MUI theme ─────────────────────────────────────────────────────────────── + +const theme = createTheme({ + palette: { primary: { main: "#1976d2" } }, + typography: { fontFamily: "Roboto, Helvetica, Arial, sans-serif" }, +}); + +// Remap library CSS tokens to MUI palette so non-registry-controlled surfaces +// (type dropdown, field name button, icons) still adopt MUI colours. +const muiTokenOverrides = ` +.jsonjoy { + --background: #ffffff; + --foreground: rgba(0, 0, 0, 0.87); + --card: #ffffff; + --card-foreground: rgba(0, 0, 0, 0.87); + --primary: #1976d2; + --primary-foreground: #ffffff; + --secondary: #f5f5f5; + --secondary-foreground: rgba(0, 0, 0, 0.87); + --muted: #f5f5f5; + --muted-foreground: rgba(0, 0, 0, 0.6); + --accent: #e3f2fd; + --accent-foreground: #1976d2; + --destructive: #d32f2f; + --destructive-foreground: #ffffff; + --border: rgba(0, 0, 0, 0.12); + --input: rgba(0, 0, 0, 0.23); + --ring: #1976d2; + --radius: 4px; + font-family: Roboto, Helvetica, Arial, sans-serif; +}`; + +// ── Component adapters ─────────────────────────────────────────────────────── + +// biome-ignore lint/suspicious/noExplicitAny: adapters intentionally accept the library's loose prop contracts +type AnyProps = any; + +const muiButtonVariant = (v?: string) => + v === "outline" + ? "outlined" + : v === "ghost" || v === "link" + ? "text" + : "contained"; + +function MuiButton({ + children, + variant, + size, + className: _c, + ...rest +}: AnyProps) { + return ( + + ); +} + +function MuiInput({ + className: _c, + type, + step, + min, + max, + "aria-invalid": ariaInvalid, + ...rest +}: AnyProps) { + // `label` arrives via {...rest} and folds into the TextField as an integrated caption. + return ( + + ); +} + +function MuiSwitch({ checked, onCheckedChange, disabled, id }: AnyProps) { + return ( + onCheckedChange?.(e.target.checked)} + /> + ); +} + +function MuiBadge({ children, variant }: AnyProps) { + return ( + + ); +} + +function MuiLabel({ children, htmlFor }: AnyProps) { + return {children}; +} + +function MuiButtonToggle({ + children, + onClick, + disabled, + "aria-pressed": ariaPressed, +}: AnyProps) { + const selected = ariaPressed === true || ariaPressed === "true"; + return ( + + {children} + + ); +} + +// ── Slot adapters ──────────────────────────────────────────────────────────── + +function MuiFieldFrame({ children, hasErrors }: AnyProps) { + return ( + + {children} + + ); +} + +function MuiFieldHeader({ children }: AnyProps) { + return ( + + {children} + + ); +} + +function MuiFieldActions({ children }: AnyProps) { + return ( + + {children} + + ); +} + +function MuiFieldBody({ children }: AnyProps) { + return ( + + {children} + + ); +} + +// ── Registry object ────────────────────────────────────────────────────────── + +const muiRegistry = { + components: { + Button: MuiButton as never, + Input: MuiInput as never, + Switch: MuiSwitch as never, + Badge: MuiBadge as never, + Label: MuiLabel as never, + ButtonToggle: MuiButtonToggle as never, + }, + slots: { + FieldFrame: MuiFieldFrame as never, + FieldHeader: MuiFieldHeader as never, + FieldActions: MuiFieldActions as never, + FieldBody: MuiFieldBody as never, + }, +}; + +// ── Sample schema ──────────────────────────────────────────────────────────── + +const sampleSchema: JsonSchema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, maxLength: 100 }, + age: { type: "integer", minimum: 0, maximum: 150 }, + email: { type: "string", format: "email" }, + isActive: { type: "boolean" }, + tags: { type: "array", items: { type: "string" } }, + metadata: { + type: "object", + properties: { created: { type: "string", format: "date-time" } }, + additionalProperties: false, + }, + }, + required: ["name", "email"], +}; + +// ── Page ───────────────────────────────────────────────────────────────────── + +const MuiRegistry = () => { + const [schema, setSchema] = useState(sampleSchema); + + return ( + + + {/* biome-ignore lint/security/noDangerouslySetInnerHtml: static CSS token override */} +