From a6bdbbb4ea1418a4c494c423eec4cc9f67a71cc5 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sat, 12 Sep 2026 22:51:31 +0200 Subject: [PATCH] feat: add custom widget flow workbench --- .agents/skills/homarr-custom-widget/SKILL.md | 16 +- .../references/runtime.md | 4 +- .../homarr-custom-widget/references/schema.md | 4 +- .../references/security.md | 2 +- apps/docs/docs/management/api/index.mdx | 25 + .../custom-widgets/agent-authoring.mdx | 13 +- .../management/custom-widgets/authoring.mdx | 76 +- .../management/custom-widgets/custom-jsx.mdx | 39 +- .../docs/management/custom-widgets/index.mdx | 7 +- .../custom-widgets/native-capabilities.mdx | 98 + .../custom-widgets/requests-and-security.mdx | 10 +- apps/docs/docs/workshop/index.mdx | 31 +- .../src/components/workshop/SubmitForm.tsx | 4 +- .../beszel-systems-studio-v3.json | 75 + .../custom-widgets/component-catalog-v1.json | 2 +- apps/nextjs/package.json | 2 + .../app/[locale]/_client-providers/trpc.tsx | 3 +- .../manage/custom-widgets/_beta-banner.tsx | 19 +- .../manage/custom-widgets/_code-editor.tsx | 58 +- .../_custom-widget-form-layout.tsx | 38 +- .../_custom-widget-form-state.tsx | 68 +- .../_custom-widget-form-utils.ts | 21 +- .../custom-widgets/_custom-widget-form.tsx | 123 +- .../_custom-widget-identifier-input.tsx | 1 + .../_custom-widget-option-fields.tsx | 4 + .../_custom-widget-preview-data.ts | 18 +- .../_custom-widget-preview-details.tsx | 124 + .../_custom-widget-preview-panel.spec.tsx | 10 +- .../_custom-widget-preview-panel.tsx | 332 +- .../_custom-widget-rename-handlers.ts | 12 +- .../_custom-widget-request-value-editors.tsx | 9 +- .../_custom-widget-requests-editor.tsx | 36 +- .../_custom-widget-saved-secrets.ts | 39 + .../_custom-widget-source-field.tsx | 10 +- .../_custom-widget-sources-editor.tsx | 47 +- .../manage/custom-widgets/_flow/README.md | 83 + .../custom-widgets/_flow/add-next-step.tsx | 129 + .../manage/custom-widgets/_flow/assistant.tsx | 127 + .../custom-widgets/_flow/binding-code.ts | 214 ++ .../custom-widgets/_flow/binding-dialog.tsx | 252 ++ .../_flow/binding-option-editor.tsx | 87 + .../_flow/binding-parameter-editor.tsx | 91 + .../_flow/binding-request-option.ts | 84 + .../custom-widgets/_flow/canvas-controls.tsx | 65 + .../_flow/canvas-editor-focus.ts | 22 + .../_flow/canvas-interactions.ts | 166 + .../custom-widgets/_flow/canvas-preview.tsx | 80 + .../custom-widgets/_flow/canvas-types.ts | 16 + .../manage/custom-widgets/_flow/canvas.tsx | 299 ++ .../manage/custom-widgets/_flow/commands.ts | 184 ++ .../_flow/connection-planner.ts | 83 + .../_flow/connection-provider.tsx | 58 + .../custom-widgets/_flow/connections.tsx | 151 + .../_flow/credential-bindings.ts | 45 + .../custom-widgets/_flow/data-bindings.tsx | 53 + .../custom-widgets/_flow/delete-selection.tsx | 103 + .../custom-widgets/_flow/document-store.ts | 218 ++ .../custom-widgets/_flow/execution-store.ts | 214 ++ .../manage/custom-widgets/_flow/execution.tsx | 73 + .../_flow/extension-commands.ts | 67 + .../_flow/extension-default-editor.tsx | 101 + .../_flow/extension-entry-editor.tsx | 257 ++ .../_flow/extension-entry-policy.ts | 45 + .../custom-widgets/_flow/extensions.tsx | 97 + .../custom-widgets/_flow/frame-controls.tsx | 35 + .../manage/custom-widgets/_flow/fullscreen.ts | 44 + .../manage/custom-widgets/_flow/graph.ts | 240 ++ .../manage/custom-widgets/_flow/group.tsx | 16 + .../manage/custom-widgets/_flow/history.ts | 51 + .../custom-widgets/_flow/identifiers.ts | 5 + .../manage/custom-widgets/_flow/inspector.tsx | 213 ++ .../custom-widgets/_flow/keybindings.ts | 103 + .../custom-widgets/_flow/layout-engine.ts | 107 + .../manage/custom-widgets/_flow/layout.ts | 109 + .../manage/custom-widgets/_flow/library.tsx | 52 + .../_flow/native-descriptor-editor.tsx | 234 ++ .../_flow/native-editor-model.ts | 106 + .../custom-widgets/_flow/native-editor.tsx | 148 + .../_flow/native-input-field.tsx | 97 + .../_flow/native-literal-input.tsx | 105 + .../_flow/native-request-test.tsx | 164 + .../custom-widgets/_flow/node-geometry.ts | 33 + .../custom-widgets/_flow/node-placement.ts | 80 + .../custom-widgets/_flow/node-port.module.css | 10 + .../custom-widgets/_flow/node-renames.ts | 31 + .../manage/custom-widgets/_flow/node.tsx | 150 + .../manage/custom-widgets/_flow/outline.tsx | 155 + .../custom-widgets/_flow/preview-controls.tsx | 131 + .../custom-widgets/_flow/preview-execution.ts | 25 + .../_flow/preview-live-actions.ts | 35 + .../_flow/preview-panel-types.ts | 15 + .../_flow/preview-presentation.tsx | 18 + .../_flow/preview-request-data.tsx | 52 + .../custom-widgets/_flow/preview-shadow.ts | 52 + .../custom-widgets/_flow/preview-state.ts | 9 + .../custom-widgets/_flow/preview-theme.tsx | 111 + .../custom-widgets/_flow/preview-widget.tsx | 59 + .../manage/custom-widgets/_flow/problems.tsx | 69 + .../manage/custom-widgets/_flow/recovery.tsx | 164 + .../_flow/request-execution.tsx | 52 + .../custom-widgets/_flow/request-journal.tsx | 82 + .../custom-widgets/_flow/resize-handle.tsx | 74 + .../custom-widgets/_flow/return-focus.ts | 10 + .../manage/custom-widgets/_flow/selection.tsx | 22 + .../_flow/source-network-hint.tsx | 52 + .../custom-widgets/_flow/start-templates.ts | 10 + .../manage/custom-widgets/_flow/start.tsx | 180 + .../manage/custom-widgets/_flow/toolbar.tsx | 128 + .../custom-widgets/_flow/workbench-types.ts | 20 + .../custom-widgets/_flow/workbench.module.css | 253 ++ .../manage/custom-widgets/_flow/workbench.tsx | 290 ++ .../custom-widgets/_flow/workshop-diff.tsx | 73 + .../manage/custom-widgets/_flow/workshop.tsx | 163 + .../_use-custom-widget-form-actions.ts | 57 +- .../_use-custom-widget-form-analysis.ts | 2 +- .../manage/custom-widgets/edit/[id]/page.tsx | 6 + .../custom-widgets/preview/[id]/page.tsx | 52 +- .../publish/[id]/_use-workshop-publication.ts | 85 + .../publish/[id]/_workshop-publish-form.tsx | 53 +- .../workshop/[id]/_workshop-detail.tsx | 71 +- .../nextjs/src/app/[locale]/manage/layout.tsx | 13 +- .../assistant/chat/assistant-chat-input.ts | 6 +- .../src/app/api/assistant/chat/route.ts | 1 + .../assistant/assistant-provider.tsx | 20 + .../assistant-tool-contracts.spec.ts | 8 + .../assistant/assistant-tool-contracts.ts | 16 + .../assistant/assistant-workbench-bridge.ts | 90 + .../src/components/board/board-switcher.tsx | 14 +- .../custom-widget-import-dialog.tsx | 10 +- .../import-integration-setup.tsx | 52 + .../custom-widgets/import-setup-panel.tsx | 26 + .../custom-widgets/install-completion.tsx | 110 + .../use-custom-widget-import.ts | 66 +- .../src/components/layout/header/burger.tsx | 18 +- .../layout/header/search.module.css | 2 +- .../components/layout/header/user-client.tsx | 12 +- .../components/layout/navigation-context.tsx | 59 + .../components/layout/navigation-frame.tsx | 83 + .../src/components/layout/navigation-link.tsx | 120 +- .../components/layout/navigation.module.css | 11 + .../src/components/layout/navigation.tsx | 10 +- apps/nextjs/src/components/layout/shell.tsx | 4 +- .../src/components/user-avatar-menu.tsx | 44 +- .../components/workshop/workshop-download.ts | 15 + apps/workshop/pb_hooks/workshop-content.js | 44 + apps/workshop/pb_hooks/workshop.pb.js | 9 + .../1789257600_widget_schema_updates.js | 27 + .../src/router/custom-widget/cache-version.ts | 3 +- .../custom-widget/content-procedures.ts | 135 + .../custom-widget/creation-procedures.ts | 25 +- .../custom-widget/custom-widget-router.ts | 160 +- .../router/custom-widget/definition-insert.ts | 17 +- .../definition-update-procedure.ts | 234 ++ .../custom-widget/management-queries.ts | 4 + .../router/custom-widget/native-catalog.ts | 73 + .../router/custom-widget/native-context.ts | 101 + .../router/custom-widget/native-dispatch.ts | 88 + .../router/custom-widget/native-execution.ts | 67 + .../router/custom-widget/native-procedures.ts | 96 + .../router/custom-widget/placed-definition.ts | 84 + .../custom-widget/preview-base-procedures.ts | 57 +- .../preview-evidence-checklist.ts | 59 + .../custom-widget/source-secret-renames.ts | 39 + .../custom-widget/stored-definition-state.ts | 66 + .../router/custom-widget/stored-definition.ts | 6 +- .../custom-widget/transfer-procedures.ts | 8 +- .../workshop-integration-setup.ts | 33 + .../custom-widget/workshop-procedures.ts | 75 +- .../router/custom-widget/workshop-service.ts | 88 + .../router/custom-widget/workshop-storage.ts | 54 + .../workshop-update-procedures.ts | 215 ++ packages/api/src/router/widgets/custom-api.ts | 131 +- packages/api/src/test/mcp.spec.ts | 5 + packages/custom-widgets/package.json | 2 + .../scripts/generate-component-metadata.ts | 5 +- packages/custom-widgets/src/core/ai-prompt.ts | 14 +- .../src/core/authoring-resources.ts | 30 +- .../src/core/component-catalog-types.ts | 2 +- .../src/core/component-catalog.generated.json | 2 +- .../src/core/component-catalog.ts | 5 +- .../src/core/custom-jsx-schema.ts | 41 +- .../custom-widgets/src/core/editor-layout.ts | 63 + .../src/core/extension-catalog.ts | 140 + .../src/core/extensions-schema.ts | 112 + .../src/core/extensions-validation.ts | 232 ++ packages/custom-widgets/src/core/import.ts | 9 +- packages/custom-widgets/src/core/index.ts | 9 + .../custom-widgets/src/core/options-schema.ts | 2 + .../src/core/portable-definition.ts | 30 + .../custom-widgets/src/core/request-schema.ts | 3 +- .../custom-widgets/src/core/scoped-styles.ts | 102 + .../src/core/source-renames-schema.ts | 11 + .../src/core/workshop-origin.ts | 19 + .../custom-widgets/src/jsx/analyzer-ast.ts | 12 +- .../custom-widgets/src/jsx/analyzer-jsx.ts | 2 +- .../src/jsx/analyzer-language.ts | 7 + .../custom-widgets/src/jsx/emitter-context.ts | 4 + .../custom-widgets/src/jsx/input-bindings.tsx | 192 ++ .../src/jsx/interpreter-foundation.ts | 4 + .../custom-widgets/src/jsx/interpreter.tsx | 31 +- .../custom-widgets/src/jsx/jsx-emitter.tsx | 43 +- packages/custom-widgets/src/jsx/policy.ts | 1 - .../src/jsx/runtime-components.tsx | 192 +- .../src/jsx/trusted-target-props.ts | 28 + .../custom-widgets/src/runtime/context.tsx | 2 + .../src/runtime/custom-jsx-renderer.tsx | 167 +- .../src/runtime/detail-views.tsx | 172 + .../runtime/execution-observer-context.tsx | 17 + .../src/runtime/execution-observer.ts | 70 + .../src/runtime/extension-environment.ts | 38 + packages/custom-widgets/src/runtime/index.ts | 7 + .../custom-widgets/src/runtime/native.tsx | 215 ++ .../src/runtime/overlay-scope.tsx | 50 + .../src/runtime/preferences.tsx | 74 + .../src/runtime/renderer-boundary.tsx | 48 + .../src/runtime/shared-content.tsx | 294 ++ .../src/runtime/source-inspection.ts | 36 + .../custom-widgets/src/runtime/sub-fetch.tsx | 31 +- packages/custom-widgets/src/runtime/types.ts | 32 + .../src/server/preview-session-template.ts | 8 +- .../src/server/preview-sessions.ts | 11 +- .../custom-widgets/src/test/ai-prompt.spec.ts | 4 +- .../src/test/authoring-resources.spec.ts | 2 +- .../custom-widgets/src/test/options.spec.ts | 1 + .../src/workbench/code-editor-types.ts | 2 + .../src/workbench/code-editor.tsx | 24 +- .../src/workbench/direct-code-mirror.tsx | 5 +- .../src/workbench/form-schema.ts | 10 +- .../src/workbench/generated-binding-edits.ts | 62 + .../src/workbench/generated-bindings.ts | 222 ++ .../custom-widgets/src/workbench/index.ts | 5 + .../src/workbench/request-references.ts | 75 +- .../src/workbench/template-insertion.ts | 32 + .../src/workbench/template-references.ts | 131 + .../mysql/0050_custom_widget_workbench.sql | 18 + .../migrations/mysql/meta/0050_snapshot.json | 2937 +++++++++++++++++ .../db/migrations/mysql/meta/_journal.json | 7 + .../0016_custom_widget_workbench.sql | 18 + .../postgresql/meta/0016_snapshot.json | 2788 ++++++++++++++++ .../migrations/postgresql/meta/_journal.json | 7 + .../sqlite/0048_custom_widget_workbench.sql | 17 + .../migrations/sqlite/meta/0048_snapshot.json | 2827 ++++++++++++++++ .../db/migrations/sqlite/meta/_journal.json | 7 + packages/db/schema/index.ts | 1 + packages/db/schema/mysql.ts | 28 + packages/db/schema/postgresql.ts | 22 + packages/db/schema/sqlite.ts | 24 + packages/db/test/mysql-migration.spec.ts | 2 +- packages/db/test/sqlite-migration.spec.ts | 1 + packages/translation/src/lang/en.json | 377 ++- packages/widgets/package.json | 3 +- ...dget-custom-widget-configuration-input.tsx | 4 + .../src/custom-api/custom-jsx-display.tsx | 45 +- .../src/custom-api/integration-option.tsx | 46 + .../custom-api/widget-definition-context.tsx | 219 +- packages/workshop/src/backend.ts | 22 +- packages/workshop/src/schema.ts | 18 +- pnpm-lock.yaml | 110 +- pnpm-workspace.yaml | 4 + 259 files changed, 23875 insertions(+), 1293 deletions(-) create mode 100644 apps/docs/docs/management/custom-widgets/native-capabilities.mdx create mode 100644 apps/docs/static/custom-widgets/beszel-systems-studio-v3.json create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-preview-details.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-saved-secrets.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/README.md create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/add-next-step.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/assistant.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/binding-code.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/binding-dialog.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/binding-option-editor.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/binding-parameter-editor.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/binding-request-option.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/canvas-controls.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/canvas-editor-focus.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/canvas-interactions.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/canvas-preview.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/canvas-types.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/canvas.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/commands.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/connection-planner.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/connection-provider.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/connections.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/credential-bindings.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/data-bindings.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/delete-selection.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/document-store.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/execution-store.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/execution.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/extension-commands.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/extension-default-editor.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/extension-entry-editor.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/extension-entry-policy.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/extensions.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/frame-controls.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/fullscreen.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/graph.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/group.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/history.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/identifiers.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/inspector.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/keybindings.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/layout-engine.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/layout.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/library.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/native-descriptor-editor.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/native-editor-model.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/native-editor.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/native-input-field.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/native-literal-input.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/native-request-test.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/node-geometry.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/node-placement.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/node-port.module.css create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/node-renames.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/node.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/outline.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-controls.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-execution.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-live-actions.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-panel-types.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-presentation.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-request-data.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-shadow.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-state.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-theme.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/preview-widget.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/problems.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/recovery.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/request-execution.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/request-journal.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/resize-handle.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/return-focus.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/selection.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/source-network-hint.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/start-templates.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/start.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/toolbar.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/workbench-types.ts create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/workbench.module.css create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/workbench.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/workshop-diff.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/_flow/workshop.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/custom-widgets/publish/[id]/_use-workshop-publication.ts create mode 100644 apps/nextjs/src/components/assistant/assistant-workbench-bridge.ts create mode 100644 apps/nextjs/src/components/custom-widgets/import-integration-setup.tsx create mode 100644 apps/nextjs/src/components/custom-widgets/import-setup-panel.tsx create mode 100644 apps/nextjs/src/components/custom-widgets/install-completion.tsx create mode 100644 apps/nextjs/src/components/layout/navigation-context.tsx create mode 100644 apps/nextjs/src/components/layout/navigation-frame.tsx create mode 100644 apps/nextjs/src/components/layout/navigation.module.css create mode 100644 apps/nextjs/src/components/workshop/workshop-download.ts create mode 100644 apps/workshop/pb_hooks/workshop-content.js create mode 100644 apps/workshop/pb_migrations/1789257600_widget_schema_updates.js create mode 100644 packages/api/src/router/custom-widget/content-procedures.ts create mode 100644 packages/api/src/router/custom-widget/definition-update-procedure.ts create mode 100644 packages/api/src/router/custom-widget/native-catalog.ts create mode 100644 packages/api/src/router/custom-widget/native-context.ts create mode 100644 packages/api/src/router/custom-widget/native-dispatch.ts create mode 100644 packages/api/src/router/custom-widget/native-execution.ts create mode 100644 packages/api/src/router/custom-widget/native-procedures.ts create mode 100644 packages/api/src/router/custom-widget/placed-definition.ts create mode 100644 packages/api/src/router/custom-widget/preview-evidence-checklist.ts create mode 100644 packages/api/src/router/custom-widget/source-secret-renames.ts create mode 100644 packages/api/src/router/custom-widget/stored-definition-state.ts create mode 100644 packages/api/src/router/custom-widget/workshop-integration-setup.ts create mode 100644 packages/api/src/router/custom-widget/workshop-service.ts create mode 100644 packages/api/src/router/custom-widget/workshop-storage.ts create mode 100644 packages/api/src/router/custom-widget/workshop-update-procedures.ts create mode 100644 packages/custom-widgets/src/core/editor-layout.ts create mode 100644 packages/custom-widgets/src/core/extension-catalog.ts create mode 100644 packages/custom-widgets/src/core/extensions-schema.ts create mode 100644 packages/custom-widgets/src/core/extensions-validation.ts create mode 100644 packages/custom-widgets/src/core/portable-definition.ts create mode 100644 packages/custom-widgets/src/core/scoped-styles.ts create mode 100644 packages/custom-widgets/src/core/source-renames-schema.ts create mode 100644 packages/custom-widgets/src/core/workshop-origin.ts create mode 100644 packages/custom-widgets/src/jsx/input-bindings.tsx create mode 100644 packages/custom-widgets/src/jsx/trusted-target-props.ts create mode 100644 packages/custom-widgets/src/runtime/detail-views.tsx create mode 100644 packages/custom-widgets/src/runtime/execution-observer-context.tsx create mode 100644 packages/custom-widgets/src/runtime/execution-observer.ts create mode 100644 packages/custom-widgets/src/runtime/extension-environment.ts create mode 100644 packages/custom-widgets/src/runtime/native.tsx create mode 100644 packages/custom-widgets/src/runtime/overlay-scope.tsx create mode 100644 packages/custom-widgets/src/runtime/preferences.tsx create mode 100644 packages/custom-widgets/src/runtime/renderer-boundary.tsx create mode 100644 packages/custom-widgets/src/runtime/shared-content.tsx create mode 100644 packages/custom-widgets/src/runtime/source-inspection.ts create mode 100644 packages/custom-widgets/src/workbench/generated-binding-edits.ts create mode 100644 packages/custom-widgets/src/workbench/generated-bindings.ts create mode 100644 packages/custom-widgets/src/workbench/template-insertion.ts create mode 100644 packages/custom-widgets/src/workbench/template-references.ts create mode 100644 packages/db/migrations/mysql/0050_custom_widget_workbench.sql create mode 100644 packages/db/migrations/mysql/meta/0050_snapshot.json create mode 100644 packages/db/migrations/postgresql/0016_custom_widget_workbench.sql create mode 100644 packages/db/migrations/postgresql/meta/0016_snapshot.json create mode 100644 packages/db/migrations/sqlite/0048_custom_widget_workbench.sql create mode 100644 packages/db/migrations/sqlite/meta/0048_snapshot.json create mode 100644 packages/widgets/src/custom-api/integration-option.tsx diff --git a/.agents/skills/homarr-custom-widget/SKILL.md b/.agents/skills/homarr-custom-widget/SKILL.md index 1e3010204..6110dd797 100644 --- a/.agents/skills/homarr-custom-widget/SKILL.md +++ b/.agents/skills/homarr-custom-widget/SKILL.md @@ -1,20 +1,20 @@ --- name: homarr-custom-widget -description: Author, validate, preview, test, install, or configure API-backed Homarr Custom JSX v2 widgets. +description: Author, validate, preview, install, or configure Homarr Custom JSX v2/v3 widgets, including static and native integration widgets. --- # Homarr Custom Widget -Author one widget or a coordinated set of widgets. Load only needed release-matched context. Run lifecycle tools alone; independent reads may run together. For a set, research once and finish each widget's validation, evidence, and persistence before the next. +Load release-matched context. Run lifecycle tools alone; independent reads may run together. For a set, research once, then validate, preview, and persist each widget before the next. -1. Read primary API documentation. Use web search when documentation is not supplied or may have changed. +1. For HTTP widgets, read primary API documentation. For native capabilities, discover installed schemas. Static widgets need no service. 2. Create credential-free definitions with keyed `sources`, `requests`, optional `options`, and safe JSX `template`. 3. While drafting, use `customWidget_validateTemplate` for focused JSX diagnostics without resending the manifest. -4. Send the definition once to `customWidget_previewCreate` and test its queries/actions. For a JSX-only fix, validate, call `customWidget_previewReviseTemplate` with its session, and retest; it inherits the manifest and resets evidence. Create a preview only for source/request/option changes. +4. Send the definition once to `customWidget_previewCreate` and test its HTTP/native queries and simulated actions. For a JSX-only fix, validate, call `customWidget_previewReviseTemplate` with its session, and retest; it inherits the manifest and resets evidence. Other definition changes require a fresh preview. 5. Configure deployment-specific source URLs and credentials through Homarr; never repeat plaintext. 6. Persist each exact final tested preview with `customWidget_createFromPreview`. Do not resend a large definition through `customWidget_create` when a preview session is available. -Treat a supplied sample or successful preview response as the binding contract. Render every core requested field, guard optional arrays and nested values before indexing, and do not silently drop returned items. Humanize numeric enums with indexed literal label arrays, omit absent numeric values instead of inventing zero, and label timestamp timezones. Give recoverable load errors and empty states a clear refresh or retry path. +Treat sample and preview responses as binding contracts. Render requested fields, guard optional values, humanize enums, and label timezones. Give errors and empty states a retry path. Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$param` for values supplied by `SubFetch`, `ActionButton`, or `ToggleSwitch`. Load queries cannot use invocation parameters. Render load queries from `data` and `status` with `RefreshButton`; reserve `SubFetch` for manual parameterized queries. Templates read `data`, `status`, `options`, and temporary `inputs`. @@ -24,11 +24,7 @@ For a bound control that depends on another input, declare its default and set ` Plan capabilities. Use one `customWidget_findComponents` search per job. Batch selected non-obvious binding or interaction documentation with `customWidget_getComponents`; reserve `customWidget_getComponent` for one unknown-prop repair. Failed validation reopens discovery; otherwise search only for a missing capability. The full catalog is for broad exploration; load at most one example. Context never limits composition. Prefer clear hierarchy, responsive grids, divided lists, and aligned actions over nested row cards. -Do not simplify a useful workflow merely because the template is interpreted. Freely compose any supported installed components with multiple sources, requests, options, `choicesFrom`, bound filters, charts, responsive detail areas, manual queries, and safe actions when they improve the user's job. Complexity must remain purposeful rather than decorative. - -Polish with a divided list or responsive media grid. Make one metric/action asymmetrically primary; keep headers compact, identity/state clear, metadata quiet, and one badge. At base artwork fills its row and caps above xs; never combine full-width media and nowrap. - -Make initial states actionable and wrap variable labels/values on narrow tiles. Do not use an unlabeled decorative icon as an empty state. +Compose installed components freely around the requested workflow. Use responsive layouts, clear hierarchy, quiet metadata, and actionable initial states. Wrap variable labels on narrow tiles and label interactive icons. Do not use imports, hooks, refs, raw HTML, raw event callbacks, browser requests, arbitrary functions, eval, bigint, npm packages, authored statement blocks, IIFEs, or recursion. Do not pretend MCP tools exist in an offline chat session. diff --git a/.agents/skills/homarr-custom-widget/references/runtime.md b/.agents/skills/homarr-custom-widget/references/runtime.md index 421391c09..8d5603348 100644 --- a/.agents/skills/homarr-custom-widget/references/runtime.md +++ b/.agents/skills/homarr-custom-widget/references/runtime.md @@ -12,7 +12,7 @@ Templates read `data.requestId`, `status.requestId`, `options.name`, and tempora ``` -Manual queries require `trigger: "manual"` on request and `SubFetch`; otherwise they run automatically. `triggerContent` with `triggerAriaLabel` makes custom content the launcher. `SubFetch` owns loading/error/retry; its child receives success plus `{ ok, status, statusText, loading: false }`. Never author `onClick` or fetch callbacks. +Manual queries require `trigger: "manual"` on request and `SubFetch`. Use `triggerContent` and `triggerAriaLabel` for a custom launcher. `SubFetch` owns loading/error/retry; its child receives success plus `{ ok, status, statusText, loading: false }`. `SubFetch`, `ActionButton`, and `ToggleSwitch` need literal `requestId`; validation rejects missing/computed IDs. @@ -35,3 +35,5 @@ Every stateful control must use `bind`, and its `inputs.` value must feed Callback parameters must not shadow the reserved roots `data`, `status`, `options`, or `inputs`. Use `` or ``; never invent components such as ``. Use expression callbacks for supported collections and trusted slots. No callback blocks, IIFEs, authored recursion, or raw events. Regex is limited to safe string operations. + +V3 adds scoped `className` styles, `View` fragments, `WidgetModal`, `WidgetDrawer`, and `AppEmbed`. Bound controls use `persist` for declared browser preferences or `content` for shared drafts saved with `ContentSaveButton`. `NativeQuery` and `NativeActionButton` invoke declared native IDs; actions retain preview simulation and integration permissions. Fetch component contracts before using these helpers. diff --git a/.agents/skills/homarr-custom-widget/references/schema.md b/.agents/skills/homarr-custom-widget/references/schema.md index 2363462b6..32607144a 100644 --- a/.agents/skills/homarr-custom-widget/references/schema.md +++ b/.agents/skills/homarr-custom-widget/references/schema.md @@ -13,7 +13,7 @@ interface HomarrCustomWidgetV2 { } ``` -The object key `default` is the required source ID; `default` is not a property on a source. Source properties are `name?`, `baseUrl`, `networkScope`, and `auth?`: +Sources are keyed by ID. Empty `sources` and `requests` support static widgets. Each HTTP request must reference an existing source; omitting its source selects `default`. Source properties are `name?`, `baseUrl`, `networkScope`, and `auth?`: ```json { @@ -47,3 +47,5 @@ Use stable real URLs for public APIs and clear suggested URLs for self-hosted se Paths use `{option:name}` and `{param:name}`; query/body references use `{ "$option": "name" }` and `{ "$param": "name" }`. Constants stay primitive (`take: 10`); `$param` is only for manual helpers, never load queries. Names and types are inferred. Every option has `label`, `control`, and `default`. Optional fields are `description`, `choices`, `choicesFrom`, `min`, `max`, `step`, `advanced`, and `group`. + +V3 uses `$schema: "homarr-custom-widget-v3"` and optional `extensions`: `stylesheet`, `fragments`, `preferences`, `content`, and `native`. Read `customWidget_schema` for their exact shapes. Integration options use `control: "integration"`, `default: ""`, and optional `integrationKinds`; installations choose local integrations. Discover native input schemas through `customWidget_nativeCapabilities`. Editor layout is separate from the manifest. diff --git a/.agents/skills/homarr-custom-widget/references/security.md b/.agents/skills/homarr-custom-widget/references/security.md index 1abec790b..c9d6e3b87 100644 --- a/.agents/skills/homarr-custom-widget/references/security.md +++ b/.agents/skills/homarr-custom-widget/references/security.md @@ -4,4 +4,4 @@ All requests use Homarr's protected server executor. Source origin, network scop The JSX interpreter blocks imports, hooks, refs, raw event callbacks, browser requests, eval, arbitrary functions, prototype access, unsafe URLs, global CSS escape, arbitrary portals, bigint, statement blocks, IIFEs, and recursion. Regex literals must be bounded and reject backreferences, lookbehind, nested quantifiers, excessive length, and unsupported flags. -Credentials are stored separately and never exported or returned to an agent. A published self-hosted source URL is only a suggestion: installers must confirm or replace private and loopback URLs for their own Homarr deployment. Source origins cannot be controlled through widget options. +Credentials stay local and are never exported. Installers confirm or replace suggested private and loopback URLs for their deployment. Source origins are fixed by configuration. diff --git a/apps/docs/docs/management/api/index.mdx b/apps/docs/docs/management/api/index.mdx index c89882c3e..541ad8c91 100644 --- a/apps/docs/docs/management/api/index.mdx +++ b/apps/docs/docs/management/api/index.mdx @@ -98,3 +98,28 @@ and focused references under `/api/custom-widgets/`. Use [Custom Widget agent authoring](../custom-widgets/agent-authoring) for the lifecycle and [MCP](../mcp) when the client supports Model Context Protocol. + +Workshop installation through `customWidget.workshopInstall` and the in-app catalog uses the same server path. Supply +`expectedRevision` from `customWidget.workshopGet` to install only the package you reviewed. Source URL overrides and +credentials are local; the installed definition retains its Workshop provenance. +For native capabilities, pass local integration selections as `integrations: { "optionId": "integrationId" }`; portable +defaults do not carry another Homarr instance's integration IDs. + +When renaming a saved source through `customWidget.update` or `customWidget.previewCreate`, include +`sourceRenames: { "previousId": "newId" }` to preserve its credentials. The server checks the original and resulting +source bindings; changed origin, network scope, or authentication requires re-entered credentials. A rename moves +encrypted credentials transactionally and does not copy them into the portable definition. + +For a linked installation, `customWidget.workshopUpdateReview` returns the current and incoming definitions, source-binding +changes, local-modification status, and review tokens. Pass its `expectedFingerprint`, `expectedState`, +`upstreamFingerprint`, and `revision` unchanged to `customWidget.workshopUpdateInstall`. Both procedures require admin +access and are exposed through MCP with an underscore replacing the dot. An intervening edit or upstream revision +returns `CONFLICT`; fetch a fresh review instead of retrying the old update. Source/authentication changes and local +customizations require a separate installation. + +Shared widget content uses `customWidget.contentRead({ itemId })` and +`customWidget.contentWrite({ itemId, name, expectedRevision, value })`. Reads require access to the board; writes require +authentication and the declaration's `modify` or `full` board permission. Each value has an independent revision; `0` +means the declared default has not been saved. A stale revision returns `CONFLICT`; read again before saving. Content is +scoped to the placed item and its selected definition. Workbench previews keep content changes in memory and cannot +persist them through these procedures. These procedures are not exposed as MCP tools. diff --git a/apps/docs/docs/management/custom-widgets/agent-authoring.mdx b/apps/docs/docs/management/custom-widgets/agent-authoring.mdx index 64e1b86bd..344c59374 100644 --- a/apps/docs/docs/management/custom-widgets/agent-authoring.mdx +++ b/apps/docs/docs/management/custom-widgets/agent-authoring.mdx @@ -26,7 +26,7 @@ Equivalent HTTP resources are available under `/api/custom-widgets/` for authent ## Authoring lifecycle 1. Load the skill and only the references needed for the design. -2. Read the external API documentation and build the definition. +2. Read the external API documentation or discover Homarr capabilities with `customWidget_nativeCapabilities`, then build the definition. 3. Validate the JSX, then create a preview from the complete definition. 4. Run every returned query and simulate every relevant action against the current preview revision. 5. Inspect the preview and redacted journal. Revise the existing preview for JSX-only changes. @@ -34,7 +34,9 @@ Equivalent HTTP resources are available under `/api/custom-widgets/` for authent 7. Configure deployment-specific source URLs and credentials, then preview the saved definition again. Use `templateLines` for multiline JSX tool input. A preview must have current evidence for every query and action before -it can be saved. Simulation counts as action evidence without performing the mutation. +it can be saved. Simulation counts as action evidence without performing the mutation. V3 native queries and actions use +`customWidget_nativeQuery` and `customWidget_nativeAction` with the owned preview session ID; their evidence is recorded +under `native:`. Native capabilities must also have current evidence before preview-based persistence. If the agent does not have a source URL or credential, it can request a short-lived Homarr configuration URL. An authenticated administrator completes it in Homarr; the agent receives the status, never the plaintext credential. @@ -43,3 +45,10 @@ Agents can also search, inspect, install, and configure existing Workshop widget See [MCP](/docs/management/mcp) for client configuration and [requests and security](./requests-and-security) for the runtime limits. + +## Editing an open workbench + +Homarr's built-in Assistant can use `workbench_read`, `workbench_patch`, and `workbench_preview` to change the visible draft. +Read before patching, supply the returned draft ID and revision, and describe one coherent change. Credentials are excluded from these +tools. The workbench validates the resulting definition and rejects stale patches; one Undo reverses an accepted patch. +Browser draft tools require an open workbench and do not grant permission to save, publish, or run live actions. diff --git a/apps/docs/docs/management/custom-widgets/authoring.mdx b/apps/docs/docs/management/custom-widgets/authoring.mdx index 809371d70..f4fe7cdc1 100644 --- a/apps/docs/docs/management/custom-widgets/authoring.mdx +++ b/apps/docs/docs/management/custom-widgets/authoring.mdx @@ -1,29 +1,67 @@ # Create a Custom Widget -The workbench provides editors for general metadata, API sources, requests, widget options, JSX, and preview data. Raw -JSON is available when direct editing is faster. +The definition remains the source of truth in the workbench. Existing widgets open without converting or rewriting their +JSX. Visual changes use targeted source edits; complex expressions remain editable in code. -## Workflow +## Definition and layout -1. Configure each API source, network scope, authentication method, and credential. -2. Add named queries and actions. Values can reference widget options or invocation parameters. -3. Define widget options and their defaults. -4. Write JSX using `data`, `status`, `options`, and temporary `inputs`. -5. Run **Test and preview**, inspect the response data and redacted request journal, then save. +A static widget needs only a JSX template: set `sources` and `requests` to `{}`. It makes no service request. -Changing the definition, options, or credentials invalidates the previous preview. Preview actions are simulated unless -**Run actions against the configured API** is enabled for that preview session. +HTTP requests run on the Homarr server. Native capabilities use Homarr's installed integrations and existing +permissions. Embeds open in the viewer's browser and need a URL reachable from that browser. -Widget options support text, textarea, number, switch, select, multi-select, slider, date, time, color, icon, URL, -duration, time zone, and JSON controls. Dynamic choices can come from a load query. +Node positions and groups are stored separately from the runtime definition. Groups do not change execution. Source +associations, data references, action invocations, and cache invalidations are distinct relationships; an invalidation edge +does not create an execution dependency. Referenced objects cannot be removed until their dependants are resolved. -See the [safe JSX runtime](./custom-jsx), [component reference](./component-reference), and -[request reference](./requests-and-security) for the authoring contract. +## Visual bindings -## Create with another AI service +Connecting a query, option, or action to the Widget opens a binding configuration. Queries can insert text, JSON, lists, +or tables; request parameters can use options, temporary controls, or fixed values. Connecting options to a request changes +its parameter bindings. These edits do not run queries or actions. -**Create or fix with any AI** copies a self-contained prompt for an ordinary ChatGPT or Claude conversation. The model -must return one fenced `json` block containing the complete definition. Paste that block into the workbench, add -credentials separately, and validate it against a real preview. +Generated JSX includes ownership comments. The canvas can disconnect or replace unchanged generated bindings as one Undo +operation. Editing a generated block or mixing it with authored references makes the relationship code-owned; resolve it +in JSX to preserve your changes. Removing ownership comments also removes visual ownership, without changing rendering. -For an MCP-connected agent, use the [Homarr Custom Widget skill](./agent-authoring) instead. +## Preview evidence + +Moving nodes, zooming, and opening inspectors do not execute requests or invalidate the preview. Editing the definition, +options, or credentials marks execution evidence stale. Valid JSX renders against the last data; invalid JSX leaves the +last valid preview visible with a stale label. Queries run deliberately, not on every keystroke. + +Preview actions simulate by default. Enabling **Run actions against the configured API** applies only to that preview +session. Native actions also require declared confirmation and their normal integration permissions. Undo changes the draft; +it never undoes or repeats a service action. + +## Undo and draft recovery + +Undo/Redo covers code, structured edits, renames, node movement, and Assistant patches. Typing is grouped and a drag or +coherent Assistant change is one operation. The bounded history excludes credentials, responses, selection, and viewport +movement. **Reset to saved** restores the saved definition and layout. Saving from an outdated workbench tab reports a +conflict rather than overwriting a newer save. +Temporary test parameters and unfinished request-body JSON keep local input Undo, so editing them does not undo an unrelated +definition change. Credential fields also retain their own input history. + +Optional browser draft recovery keeps a credential-free draft and editor layout. Recovery is local to the browser and +requires a valid recoverable configuration. It is not a revision archive or a backup of service data. + +## Assistant changes + +The Assistant reads the visible draft and selection, edits through shared draft commands, and can run preview queries. A stale patch is +rejected when the draft has changed. Saving, live service actions, and Workshop publication remain separate operations. + +Form view retains **Create or fix with any AI** for external AI services. Configure credentials locally after importing +the returned definition and validate the result. + +## Advanced capabilities + +Native capabilities, scoped CSS, reusable view fragments, typed preferences, and shared content use +`homarr-custom-widget-v3`; ordinary HTTP and static widgets remain valid v2 definitions. Native input bindings must match +the installed capability's schema. See the [runtime](./custom-jsx) for the expression language and extension contracts. + +V3 adds an integration picker with optional `integrationKinds`. The integration list respects the +current user's access. Portable exports clear local integration selections so installers choose their own services. + +See the [runtime](./custom-jsx), [component reference](./component-reference), +[native capabilities](./native-capabilities), and [request reference](./requests-and-security). diff --git a/apps/docs/docs/management/custom-widgets/custom-jsx.mdx b/apps/docs/docs/management/custom-widgets/custom-jsx.mdx index 7e3424858..a9fdd67f2 100644 --- a/apps/docs/docs/management/custom-widgets/custom-jsx.mdx +++ b/apps/docs/docs/management/custom-widgets/custom-jsx.mdx @@ -7,7 +7,7 @@ Templates are JSX expressions interpreted inside a widget-local safety and rende - `options.name` — saved widget configuration - `inputs.name` — temporary values from declarative `bind` -The v2 manifest requires a `default` source and supports up to eight sources, 64 requests, and 64 options. Source IDs are +Manifests support up to eight sources, 64 requests, and 64 options. A static widget can use empty `sources` and `requests`; every HTTP request must name an existing source. Source IDs are case-insensitively unique. Each request allows up to 32 static headers and 32 invalidation targets; a widget can contain up to 24 secrets, each up to 8,192 characters, and templates are limited to 50,000 characters. Validation reports the exact field when a limit is exceeded. @@ -41,7 +41,7 @@ before it reaches `data`, so treat it as untrusted input even when the source is ``` -Bound values live only in React memory while the widget is mounted. They are not saved to the database, board options, exports, browser storage, or `localStorage`. Remounting resets them. Components can share a binding when their adapter types agree. +Without an explicit v3 `persist` binding, bound values live only in React memory while the widget is mounted. They are not saved to the database, board options, exports, browser storage, or `localStorage`. Remounting resets them. Components can share a binding when their adapter types agree. Bindable controls accept a scalar `resetKey`. When it changes, Homarr restores that control to its declared `defaultValue` or `defaultChecked` without running a request. This keeps dependent pagination correct while a manual `SubFetch` waits for the user to submit the new query. @@ -74,3 +74,38 @@ than one variable quantifier are rejected. - `Icon`/`TablerIcon` resolves installed safe icons. Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile. + +## Version 3 capabilities + +Use `$schema: "homarr-custom-widget-v3"` for `extensions`, integration options, scoped classes, and the components below. Version 2 definitions continue to render; opening them in the workbench does not upgrade their runtime schema. Editor positions are Homarr metadata and are excluded from the portable definition. + +`extensions.stylesheet` accepts up to 30,000 characters. `className` references local classes; selectors and keyframe names are scoped per widget instance, including content opened through `WidgetModal` and `WidgetDrawer`. Use flat rules, `@media`, `@supports`, `@container`, and local `@keyframes`. Resource-loading CSS (`url()`, imports, fonts), global registration rules, and unresolved animation names are rejected. Theme CSS variables remain available. Inline style restrictions still apply. + +Templates can read `theme.colorScheme`, `theme.primaryColor`, `theme.colors`, `theme.spacing`, `theme.radius`, and `theme.fontFamily`. `container.width` and `container.height` report the widget content size in CSS pixels, so responsive composition can follow its tile rather than the browser viewport. + +Declare reusable JSX in `extensions.fragments` and render it with ``. The fragment reads `props.label` and inherits the normal data bindings. Fragment names are literal, recursion is rejected, and all expansions share the interpreter's existing operation budget. Up to 32 fragments and 100,000 combined JSX characters are supported. + +```json +{ + "preferences": { + "filter": { "type": "string", "defaultValue": "" } + } +} +``` + +The fragment above belongs under `extensions`. A matching `` exposes the value through both `inputs.filter` and `preferences.filter`. Preferences are typed, scoped to the board item, and saved only in that browser; another account or device gets its own values. Preview preferences remain in memory. Exports include the declarations and defaults, never the user's saved values. Storage-disabled browsers retain an in-memory fallback. `resetKey` resets a persisted control to its declared preference default. + +`WidgetModal` and `WidgetDrawer` require `title` and `triggerLabel`; Homarr owns their open/close behavior, focus handling, and portal boundary. They allow detailed views without authored callbacks. +Supported selects, menus, tooltips, and popovers use Homarr-owned dropdown targets to escape tile clipping while preserving +widget styles and the preview theme. Nested dropdowns stay within their parent overlay. Authored event handlers, refs, and +portal targets remain blocked; Mantine's own trigger handlers work through the component adapters. + +`AppEmbed` requires an absolute HTTP(S) `src` and an accessible `title`. Its URL is reached by the browser, whereas HTTP sources are reached by the Homarr server; a Docker service hostname usually cannot be used for both. The embedded service must permit framing. The iframe uses an isolated origin and restricts navigation; apps requiring same-origin storage or unrestricted browser capabilities need their normal external link instead. `allowForms` explicitly enables embedded form submission. + +`extensions.native` maps local IDs to curated Homarr capabilities. Integration-backed entries reference a saved `integration` option through `integrationOption`; the server applies the existing integration and board permissions. `{(systems, meta) => {systems.length}}` uses the same loading/error behavior as `SubFetch`. Use `trigger="manual"` for invocation parameters and `live` for a supported subscription. Automatic results populate `data` and `status` under their native ID; manual results remain local to the component. + +Native action entries require `kind: "action"`, `trigger: "manual"`, a `modify` or `full` permission, and a `confirmation` object containing `title` and `message`. Invoke them through `NativeActionButton`. Preview actions use the existing simulation mode; declaring an action does not authorize a live mutation. Native capability IDs and HTTP request IDs must be distinct. + +The [Beszel systems studio definition](/custom-widgets/beszel-systems-studio-v3.json) combines systems, configurable columns, a saved system selection, historical charts, and live detail views. Select a local Beszel integration after importing it. The example uses `preferences` directly for values needed before a control mounts; `inputs` appears only after its bound control mounts. Live failures keep the last received event and expose `meta.stale` alongside Retry. + +Shared editable content uses a separate `extensions.content` declaration with the same types/defaults as preferences plus `permission: "modify"` or `"full"`. `