{"_id":"@davidrandoll/automation-engine-ui","name":"@davidrandoll/automation-engine-ui","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@davidrandoll/automation-engine-ui","version":"0.1.0","type":"module","description":"Embeddable React pages for the Automation Engine: Builder, User-Defined Types, Playground.","main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","publishConfig":{"access":"public"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./styles.css":"./dist/style.css","./src/index.ts":"./src/index.ts","./src/styles.css":"./src/styles.css"},"sideEffects":["**/*.css","**/widgets/*"],"scripts":{"build":"vite build","pack:lib":"vite build && npm pack","prepublishOnly":"vite build"},"peerDependencies":{"react":"^18 || ^19","react-dom":"^18 || ^19","react-hook-form":"^7"},"dependencies":{"@dnd-kit/core":"^6.3.1","@dnd-kit/sortable":"^10.0.0","@dnd-kit/utilities":"^3.2.2","@monaco-editor/react":"^4.7.0","@radix-ui/react-accordion":"^1.2.12","@radix-ui/react-collapsible":"^1.1.12","@radix-ui/react-dialog":"^1.1.15","@radix-ui/react-scroll-area":"^1.2.10","@radix-ui/react-select":"^2.2.6","@radix-ui/react-slot":"^1.2.4","@radix-ui/react-tabs":"^1.1.13","@radix-ui/react-tooltip":"^1.2.8","@tailwindcss/vite":"^4.0.6","@types/js-yaml":"^4.0.9","@xyflow/react":"^12.10.0","class-variance-authority":"^0.7.1","clsx":"^2.1.1","framer-motion":"^12.23.25","js-yaml":"^4.1.1","lucide-react":"^0.545.0","react-icons":"^5.5.0","sonner":"^2.0.7","tailwind-merge":"^3.4.0","tailwindcss":"^4.0.6"},"devDependencies":{"@types/node":"^22.10.2","@types/react":"^19.2.0","@types/react-dom":"^19.2.0","@vitejs/plugin-react":"^5.0.4","postcss-prefix-selector":"^2.1.1","react":"^19.2.0","react-dom":"^19.2.0","react-hook-form":"^7.68.0","tw-animate-css":"^1.4.0","typescript":"^5.7.2","vite":"^7.1.7","vite-plugin-dts":"^4.5.4"},"gitHead":"151e66abcddc09a758a4e6df082c0159e9d0e52a","_id":"@davidrandoll/automation-engine-ui@0.1.0","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-QGYjr4phjzMRvPYfOmJr/EybibuONWb2l0MFM27Gxn50A7sWab1jIlzSXCn9TXWFMGgayOvXLt+J11ark0wSmw==","shasum":"041dbb77d61aabe8923fb661107d0d1ae1bb7da6","tarball":"https://registry.npmjs.org/@davidrandoll/automation-engine-ui/-/automation-engine-ui-0.1.0.tgz","fileCount":63,"unpackedSize":1468803,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCID8DwiNHcpYqhKG/ARzuMHmoWpJiiM89Crvo4UkNY1zLAiEAp4NuV8KYeV4h1m1qzwb0Py3VVRMQSJp7NgVfhj+AomY="}]},"_npmUser":{"name":"davidrandoll9","email":"davidrandoll999@gmail.com"},"directories":{},"maintainers":[{"name":"davidrandoll9","email":"davidrandoll999@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/automation-engine-ui_0.1.0_1777597975663_0.3136321781153344"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-01T01:12:55.584Z","0.1.0":"2026-05-01T01:12:55.888Z","modified":"2026-05-01T01:12:56.149Z"},"maintainers":[{"name":"davidrandoll9","email":"davidrandoll999@gmail.com"}],"description":"Embeddable React pages for the Automation Engine: Builder, User-Defined Types, Playground.","readme":"# @davidrandoll/automation-engine-ui\n\nEmbeddable React components for the Automation Engine. Drop the visual / code / workflow automation editors and the user-defined-block management UI into your own React app, wired to your own routes and styled with your own theme.\n\n- **Per-mode editors** — use just the visual form, just the JSON/YAML editor, or just the workflow canvas.\n- **Controlled-via-callback** — every component takes a `value` and emits an `onChange`; you own the data.\n- **Bring your own routing** — components never call `useNavigate()`. They expose callbacks (`onSuccess`, `onTabChange`, etc.) that you wire into your router.\n- **Scoped styles** — design tokens live under a `.ae-root` class, not `:root`, so they don't collide with your design system.\n\n---\n\n## Table of contents\n\n- [Install](#install)\n- [Quick start](#quick-start)\n- [Backend requirement](#backend-requirement)\n- [Configuration](#configuration)\n- [Components](#components)\n  - [`AutomationInteractive`](#automationinteractive) – form-based editor\n  - [`AutomationCode`](#automationcode) – JSON / YAML editor\n  - [`AutomationWorkflow`](#automationworkflow) – visual canvas\n  - [`ModeSelector`](#modeselector) – tab switcher between the three modes\n  - [`UserDefinedPage`](#userdefinedpage) – list/manage user-defined blocks\n  - [`UserDefinedRegisterPage`](#userdefinedregisterpage) – create form\n  - [`UserDefinedEditPage`](#userdefinededitpage) – edit form\n  - [`PlaygroundPage`](#playgroundpage) – run automations + view traces\n- [Building a builder page (composition example)](#building-a-builder-page-composition-example)\n- [Custom widgets](#custom-widgets)\n- [Theming and styles](#theming-and-styles)\n- [Toasts](#toasts)\n- [Monaco editor](#monaco-editor)\n- [Type reference](#type-reference)\n- [Limitations](#limitations)\n- [Troubleshooting](#troubleshooting)\n\n---\n\n## Install\n\n```bash\nnpm install @davidrandoll/automation-engine-ui\n```\n\nPeer dependencies your host app must provide:\n\n| Package | Version |\n|---|---|\n| `react` | `^18` or `^19` |\n| `react-dom` | `^18` or `^19` |\n| `react-hook-form` | `^7` |\n\nEverything else (Radix UI, lucide-react, Monaco, xyflow, sonner, js-yaml, Tailwind v4 runtime, etc.) is bundled inside the package — you do not need to install them separately.\n\n---\n\n## Quick start\n\n```tsx\nimport {\n  AutomationEngineConfigProvider,\n  AutomationInteractive,\n} from \"@davidrandoll/automation-engine-ui\";\nimport \"@davidrandoll/automation-engine-ui/styles.css\";\nimport { Toaster } from \"sonner\";\n\nexport function App() {\n  return (\n    <AutomationEngineConfigProvider\n      config={{\n        apiBaseUrl: \"https://api.example.com\",\n        apiPath: \"/automation-engine\",\n      }}\n    >\n      <AutomationInteractive\n        onChange={(automation) => console.log(\"user edited:\", automation)}\n      />\n      <Toaster richColors position=\"top-right\" />\n    </AutomationEngineConfigProvider>\n  );\n}\n```\n\nThat's the minimum: provider for API config, one editor, one CSS import, one Toaster (recommended). Everything else is optional.\n\n---\n\n## Backend requirement\n\nThese components are a thin client over the Automation Engine HTTP API. They expect endpoints under `{apiBaseUrl}{apiPath}` such as:\n\n- `GET /automation-definition/schema` — schema for the automation form\n- `GET /block/{area}` — list available blocks for an area\n- `GET /block/{name}/schema` — schema for a specific block\n- `GET|POST|PUT|DELETE /user-defined/{type}` — user-defined block CRUD\n- `POST /playground/execute` — execute an automation and return a trace\n\nIf your backend is on a different origin, configure CORS on the server. The client sends `Content-Type: application/json` and no auth headers — your backend should handle session/cookie auth, or you can extend the agent (see [escape hatch](#configuration) below).\n\n---\n\n## Configuration\n\n### `AutomationEngineConfigProvider`\n\nWrap any subtree that mounts components from this package once.\n\n```tsx\nimport { AutomationEngineConfigProvider } from \"@davidrandoll/automation-engine-ui\";\n\n<AutomationEngineConfigProvider\n  config={{\n    apiBaseUrl: \"https://api.example.com\", // origin\n    apiPath: \"/automation-engine\",         // path prefix\n  }}\n>\n  {/* … your pages … */}\n</AutomationEngineConfigProvider>\n```\n\n| Prop | Type | Default | Notes |\n|---|---|---|---|\n| `config.apiBaseUrl` | `string` | `window.location.origin + contextPath` | Absolute origin or absolute URL |\n| `config.apiPath` | `string` | `\"/automation-engine\"` | Appended to `apiBaseUrl` for every request |\n| `config.contextPath` | `string` | from `window.__APP_CONFIG__` | Used when `apiBaseUrl` is omitted |\n| `config.uiPath` | `string` | from `window.__APP_CONFIG__` | Reserved for legacy server-side bootstrap |\n\nThe provider is optional. If omitted, the package falls back to `window.__APP_CONFIG__` (set by some server bootstraps), then to defaults.\n\n### Escape hatch: `configureAgent`\n\nFor non-React bootstraps (e.g. a CDN-style script), call `configureAgent` once before mounting any component:\n\n```ts\nimport { configureAgent } from \"@davidrandoll/automation-engine-ui\";\n\nconfigureAgent({\n  apiBaseUrl: \"https://api.example.com\",\n  apiPath: \"/automation-engine\",\n});\n```\n\nThe provider does this for you internally.\n\n---\n\n## Components\n\n### `AutomationInteractive`\n\nForm-based editor with a live JSON/YAML preview pane. No header, no mode toggle.\n\n```tsx\nimport { AutomationInteractive } from \"@davidrandoll/automation-engine-ui\";\n\n<AutomationInteractive\n  value={initialAutomation}\n  onChange={(next) => setMyData(next)}\n/>\n```\n\n#### Props\n\n| Prop | Type | Default | Notes |\n|---|---|---|---|\n| `value` | `Record<string, unknown>` \\| `string` \\| `undefined` | `undefined` | Object, JSON string, or YAML string. **Initial-only** — see below. |\n| `format` | `\"json\"` \\| `\"yaml\"` | `\"json\"` if `value` is a string | Required when `value` is a string. |\n| `onChange` | `(value: Record<string, unknown>) => void` | `undefined` | Fires on every user edit. Receives a parsed object regardless of input format. |\n| `className` | `string` | `undefined` | Appended to the root `<div className=\"ae-root\">`. |\n\n#### Initial-only value semantics\n\n`value` is applied **once on mount**. Subsequent changes to the prop do not re-seed the form. This avoids cursor-jumping echo loops where `onChange` updates `value` which updates the form which triggers `onChange` again. If you need to load `value` asynchronously, render the editor only after the value resolves:\n\n```tsx\nconst [draft, setDraft] = useState<Automation | null>(null);\nuseEffect(() => {\n  fetch(\"/api/draft\").then((r) => r.json()).then(setDraft);\n}, []);\nreturn draft && <AutomationInteractive value={draft} onChange={setDraft} />;\n```\n\n---\n\n### `AutomationCode`\n\nJSON/YAML Monaco editor with split-pane and copy/download buttons.\n\n```tsx\n<AutomationCode\n  value={`alias: My Automation\\ntriggers:\\n  - trigger: alwaysTrue\\n`}\n  format=\"yaml\"\n  onChange={(obj) => save(obj)}\n/>\n```\n\nSame props as `AutomationInteractive`, plus:\n\n| Prop | Type | Default | Notes |\n|---|---|---|---|\n| `initialFormat` | `\"json\"` \\| `\"yaml\"` | `\"yaml\"` | Which view the code editor opens in (the user can still toggle inside). |\n\n`onChange` fires after each Monaco edit and always emits a parsed object — your handler doesn't have to choose between JSON and YAML.\n\n---\n\n### `AutomationWorkflow`\n\nVisual node-and-edge canvas for the automation, built on `@xyflow/react`.\n\n```tsx\n<AutomationWorkflow\n  value={automation}\n  onChange={setAutomation}\n  className=\"h-screen\"\n/>\n```\n\nSame props as `AutomationInteractive`. Best given a tall container; the component fills its parent.\n\n---\n\n### `ModeSelector`\n\nA pre-styled tabbed control for switching between `interactive`, `code`, and `workflow`. Optional — use this if you want the same visual style as the reference app, or roll your own.\n\n```tsx\nimport { ModeSelector, type UIMode } from \"@davidrandoll/automation-engine-ui\";\n\nconst [mode, setMode] = useState<UIMode>(\"interactive\");\n\n<ModeSelector mode={mode} onModeChange={setMode} />\n```\n\n| Prop | Type | Notes |\n|---|---|---|\n| `mode` | `\"interactive\"` \\| `\"code\"` \\| `\"workflow\"` | Currently selected tab. |\n| `onModeChange` | `(mode: UIMode) => void` | Fires when the user clicks a tab. |\n\n---\n\n### `UserDefinedPage`\n\nList + manage user-defined Actions / Conditions / Triggers / Variables. Each tab loads its definitions from the backend; users can register, edit, or unregister.\n\n```tsx\nimport { UserDefinedPage, type BlockType } from \"@davidrandoll/automation-engine-ui\";\n\n<UserDefinedPage\n  defaultTab=\"actions\"\n  onRegisterBlock={(type) => router.push(`/user-defined/${type}/new`)}\n  onEditBlock={(type, name) => router.push(`/user-defined/${type}/edit?name=${name}`)}\n  onTabChange={(type) => router.push(`/user-defined/${type}`)}\n/>\n```\n\n| Prop | Type | Default | Notes |\n|---|---|---|---|\n| `activeTab` | `BlockType` | undefined | Controlled tab. If set, the component does **not** manage its own tab state. |\n| `defaultTab` | `BlockType` | `\"actions\"` | Initial tab when uncontrolled. |\n| `onTabChange` | `(blockType: BlockType) => void` | `undefined` | Fires on tab click. Wire to your router. |\n| `onRegisterBlock` | `(blockType: BlockType) => void` | `undefined` | Fires on the \"Register {Type}\" button. |\n| `onEditBlock` | `(blockType: BlockType, name: string) => void` | `undefined` | Fires on a row's Edit button. |\n\n`BlockType = \"actions\" | \"conditions\" | \"triggers\" | \"variables\"`.\n\n---\n\n### `UserDefinedRegisterPage`\n\nForm for creating a new user-defined block.\n\n```tsx\n<UserDefinedRegisterPage\n  blockType=\"actions\"\n  onSuccess={() => router.push(\"/user-defined/actions\")}\n  onCancel={() => router.back()}\n/>\n```\n\n| Prop | Type | Notes |\n|---|---|---|\n| `blockType` | `BlockType` | Which kind of block to register. **Required.** |\n| `onSuccess` | `() => void` | Fires after a successful POST. Navigate the user away here. |\n| `onCancel` | `() => void` | Fires on the back arrow. |\n\nA success toast (`\"Action 'foo' registered successfully\"`) is emitted via `sonner` — mount a `<Toaster />` somewhere in your app or those messages will be silent.\n\n---\n\n### `UserDefinedEditPage`\n\nForm for editing an existing user-defined block. Loads the existing definition + schema in parallel on mount.\n\n```tsx\n<UserDefinedEditPage\n  blockType=\"actions\"\n  name=\"my-action\"\n  onSuccess={() => router.push(\"/user-defined/actions\")}\n  onCancel={() => router.back()}\n/>\n```\n\nSame props as `UserDefinedRegisterPage`, plus:\n\n| Prop | Type | Notes |\n|---|---|---|\n| `name` | `string` | The identifier of the block being edited. **Required.** |\n\n---\n\n### `PlaygroundPage`\n\nRun an automation against the backend and view its trace, or paste a trace JSON and visualize it without running anything. Includes Monaco editors for the automation, inputs, and trace; a node graph for the trace; and a logs viewer.\n\n```tsx\n<PlaygroundPage className=\"h-screen\" />\n```\n\n| Prop | Type | Notes |\n|---|---|---|\n| `className` | `string` | Appended to the root flex container. Give it height. |\n\nThis page is self-contained — it manages its own automation/inputs/trace state internally. There is no `value` / `onChange` for the playground; it's a tool, not a controlled editor.\n\n---\n\n## Building a builder page (composition example)\n\nThe package intentionally **does not** ship an \"all-in-one\" builder. The reference standalone app builds one by composing the three primitives + `ModeSelector` like this:\n\n```tsx\nimport { useState } from \"react\";\nimport {\n  AutomationInteractive,\n  AutomationCode,\n  AutomationWorkflow,\n  ModeSelector,\n  type AutomationValue,\n  type UIMode,\n} from \"@davidrandoll/automation-engine-ui\";\n\nexport function BuilderPage() {\n  const [mode, setMode] = useState<UIMode>(\"interactive\");\n  const [data, setData] = useState<AutomationValue | undefined>(undefined);\n\n  return (\n    <div className=\"p-6 bg-gray-50 min-h-screen\">\n      <header className=\"flex justify-between items-center mb-6\">\n        <div>\n          <h1 className=\"text-2xl font-bold\">My Automation Builder</h1>\n          <p className=\"text-gray-500\">Pick a mode and start building.</p>\n        </div>\n        <ModeSelector mode={mode} onModeChange={setMode} />\n      </header>\n\n      {mode === \"interactive\" && <AutomationInteractive value={data} onChange={setData} />}\n      {mode === \"code\"        && <AutomationCode        value={data} onChange={setData} />}\n      {mode === \"workflow\"    && <AutomationWorkflow    value={data} onChange={setData} />}\n    </div>\n  );\n}\n```\n\nThe data is held at the page level (`data` state), so switching modes preserves what the user has typed. Each editor seeds itself from `data` on mount and reports back via `onChange`.\n\nIf you only want one mode, mount one component — no need for the wrapper at all:\n\n```tsx\n<AutomationWorkflow value={automation} onChange={save} className=\"h-screen\" />\n```\n\n---\n\n## Custom widgets\n\nSchemas can request a custom field widget via `@PresentationHint(customComponent=\"myWidget\")`. Register your component:\n\n```tsx\nimport { registerCustomWidget, type CustomWidgetProps } from \"@davidrandoll/automation-engine-ui\";\n\nfunction ColorPicker({ name, value, onChange, label }: CustomWidgetProps) {\n  return (\n    <label>\n      {label}\n      <input\n        type=\"color\"\n        name={name}\n        value={(value as string) ?? \"#000000\"}\n        onChange={(e) => onChange(e.target.value)}\n      />\n    </label>\n  );\n}\n\nregisterCustomWidget(\"colorPicker\", ColorPicker);\n```\n\n`CustomWidgetProps`:\n\n```ts\ninterface CustomWidgetProps {\n  name: string;                            // form field name\n  value: unknown;                          // current value\n  onChange: (value: unknown) => void;      // setter\n  control: Control<FieldValues>;           // react-hook-form Control\n  schema: Record<string, unknown>;         // raw field schema\n  customProps?: Record<string, unknown>;   // anything from @PresentationHint\n  label: string;\n}\n```\n\nA bundled `colorPicker` widget is auto-registered the first time any field renders, so existing `customComponent=\"colorPicker\"` references work without configuration.\n\n---\n\n## Theming and styles\n\nThe package ships a single CSS file:\n\n```ts\nimport \"@davidrandoll/automation-engine-ui/styles.css\";\n```\n\nDifferences from a typical Tailwind app:\n\n- **No global preflight reset.** The CSS does not touch `body`, `html`, or `*` — it won't fight your existing reset.\n- **Tokens are scoped.** Design tokens (`--background`, `--primary`, `--border`, etc.) live under `.ae-root`, not `:root`. Every component renders this class on its top-level element. Define your own `:root` tokens freely without collision.\n- **Dark mode.** Apply `.dark` to either `.ae-root` itself or to any ancestor — the components react to both `.ae-root.dark` and `.dark .ae-root`.\n\nTo **override a token**, target `.ae-root` in your own stylesheet:\n\n```css\n.ae-root {\n  --primary: oklch(0.55 0.2 264);\n  --radius: 0.25rem;\n}\n```\n\nThe components use Tailwind v4 utility classes internally; the bundled CSS already contains every utility they need.\n\n---\n\n## Toasts\n\nSeveral actions emit toast notifications (`Action 'foo' registered`, `Failed to load triggers`, etc.) via [sonner](https://sonner.emilkowal.ski/). Mount a `<Toaster />` once somewhere above the components:\n\n```tsx\nimport { Toaster } from \"sonner\";\n\n<>\n  <Toaster richColors position=\"top-right\" />\n  {/* … your pages … */}\n</>\n```\n\n`sonner` is a peer-friendly transitive dependency; you don't need to install it separately, but you do need to render the `<Toaster />`.\n\n---\n\n## Monaco editor\n\n`AutomationCode` and `PlaygroundPage` use [`@monaco-editor/react`](https://github.com/suren-atoyan/monaco-react). By default it loads the Monaco runtime from a CDN (`https://cdn.jsdelivr.net/npm/monaco-editor`) at first render.\n\nIf your host app already configures Monaco (or is offline-only and needs a local copy), do this once before rendering any component that uses it:\n\n```tsx\nimport { loader } from \"@monaco-editor/react\";\n\nloader.config({ paths: { vs: \"/vendor/monaco/min/vs\" } });\n```\n\n---\n\n## Type reference\n\n```ts\n// Re-exported from the package\nexport type AutomationValue = Record<string, unknown>;\n\nexport type UIMode = \"interactive\" | \"code\" | \"workflow\";\n\nexport type BlockType = \"actions\" | \"conditions\" | \"triggers\" | \"variables\";\n\nexport interface AutomationModeProps {\n  value?: AutomationValue | string;\n  format?: \"json\" | \"yaml\";\n  onChange?: (value: AutomationValue) => void;\n  className?: string;\n}\n\nexport interface AutomationEngineConfig {\n  apiBaseUrl?: string;\n  apiPath?: string;\n  contextPath?: string;\n  uiPath?: string;\n}\n\nexport interface CustomWidgetProps {\n  name: string;\n  value: unknown;\n  onChange: (value: unknown) => void;\n  control: Control<FieldValues>;\n  schema: Record<string, unknown>;\n  customProps?: Record<string, unknown>;\n  label: string;\n}\n\n// User-defined block definition shapes\nexport interface BaseDefinition {\n  alias?: string;\n  description?: string;\n  name: string;\n  parameters?: Record<string, unknown>;\n}\nexport interface UserDefinedActionDefinition    extends BaseDefinition { /* … */ }\nexport interface UserDefinedConditionDefinition extends BaseDefinition { /* … */ }\nexport interface UserDefinedTriggerDefinition   extends BaseDefinition { /* … */ }\nexport interface UserDefinedVariableDefinition  extends BaseDefinition { /* … */ }\nexport type UserDefinedDefinition =\n  | UserDefinedActionDefinition\n  | UserDefinedConditionDefinition\n  | UserDefinedTriggerDefinition\n  | UserDefinedVariableDefinition;\n```\n\n---\n\n## Limitations\n\n- **No router included.** Wire callback props (`onModeChange`, `onTabChange`, `onRegisterBlock`, `onEditBlock`, `onSuccess`, `onCancel`) into your host's router.\n- **`value` is initial-only**, not controlled. See [Initial-only value semantics](#initial-only-value-semantics).\n- **One backend per page.** `configureAgent` writes to a module-level singleton, so mounting two component trees against different backends in the same browser tab is not supported.\n- **Auth.** The HTTP client sends no auth headers. Your backend needs to handle session cookies / CORS, or fork the package and extend the agent to attach tokens.\n- **Server-side rendering.** Components render `null`-safe on first paint but do not pre-render their data; expect a \"Loading…\" flash on first mount.\n\n---\n\n## Troubleshooting\n\n**The page renders but buttons / inputs are unstyled.**\nYou forgot to `import \"@davidrandoll/automation-engine-ui/styles.css\"`. The bundled CSS is what supplies all utility classes.\n\n**`useAutomationEngineConfig must be used within an AutomationEngineConfigProvider`**\nYou called the hook outside the provider. Wrap your subtree in `<AutomationEngineConfigProvider>`. (Note: components themselves don't require the provider — the hook does, and only if you choose to consume the config in your own code.)\n\n**API requests 404 against the wrong URL.**\nPass `config={{ apiBaseUrl, apiPath }}` to `<AutomationEngineConfigProvider>`. Without it the package builds URLs from `window.location.origin + window.__APP_CONFIG__.contextPath + apiPath` — which only works when your host runs behind the Spring backend.\n\n**Toast messages never appear.**\nMount a `<Toaster />` from `sonner` in your app tree.\n\n**Custom widget renders as a plain input.**\nThe widget name in your schema (`@PresentationHint(customComponent=\"x\")`) must match the name passed to `registerCustomWidget(\"x\", …)` exactly. Register the widget before the page mounts; once at app boot is fine.\n\n**Monaco editor never loads.**\nYou're either offline (default loader uses a CDN) or you have a CSP blocking `cdn.jsdelivr.net`. Either allow it or call `loader.config({ paths: { vs: \"/your/local/path\" } })` before rendering the playground or code editor.\n","readmeFilename":"README.md","_rev":"1-0b44e057fd075f8ad68049d9c5438e1f"}