| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280 |
- import type {
- Catalog,
- ComputedFunction,
- DirectiveDefinition,
- SchemaDefinition,
- Spec,
- StateStore,
- UIElement,
- } from "@json-render/core";
- import type { Component, Snippet } from "svelte";
- import type {
- BaseComponentProps,
- EventHandle,
- SetState,
- StateModel,
- } from "./catalog-types.js";
- import CatalogRenderer from "./CatalogRenderer.svelte";
- /**
- * Props passed to component renderers
- */
- export interface ComponentRenderProps<P = Record<string, unknown>> {
- /** The element being rendered */
- element: UIElement<string, P>;
- /** Rendered children snippet */
- children?: Snippet;
- /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. */
- emit: (event: string) => void;
- /** Get an event handle with metadata */
- on: (event: string) => EventHandle;
- /**
- * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
- * Maps prop name → absolute state path for write-back.
- */
- bindings?: Record<string, string>;
- /** Whether the parent is loading */
- loading?: boolean;
- }
- /**
- * Component renderer type - a Svelte component that receives ComponentRenderProps
- */
- export type ComponentRenderer<P = Record<string, unknown>> = Component<
- ComponentRenderProps<P>
- >;
- /**
- * Registry of component renderers.
- * Maps component type names to Svelte components.
- */
- export type ComponentRegistry = Record<string, ComponentRenderer<any>>;
- /**
- * Action handler function for defineRegistry
- */
- type DefineRegistryActionFn = (
- params: Record<string, unknown> | undefined,
- setState: SetState,
- state: StateModel,
- ) => Promise<void>;
- /**
- * Result returned by defineRegistry
- */
- export interface DefineRegistryResult {
- /** Component registry for Renderer */
- registry: ComponentRegistry;
- /**
- * Create ActionProvider-compatible handlers.
- */
- handlers: (
- getSetState: () => SetState | undefined,
- getState: () => StateModel,
- ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;
- /**
- * Execute an action by name imperatively
- */
- executeAction: (
- actionName: string,
- params: Record<string, unknown> | undefined,
- setState: SetState,
- state?: StateModel,
- ) => Promise<void>;
- }
- /**
- * Create a registry from a catalog with Svelte components and/or actions.
- *
- * Components must accept `BaseComponentProps` as their props interface.
- *
- * @example
- * ```ts
- * import { defineRegistry } from "@json-render/svelte";
- * import Card from "./components/Card.svelte";
- * import Button from "./components/Button.svelte";
- * import { myCatalog } from "./catalog";
- *
- * const { registry, handlers } = defineRegistry(myCatalog, {
- * components: {
- * Card,
- * Button,
- * },
- * actions: {
- * submit: async (params, setState) => {
- * // handle action
- * },
- * },
- * });
- * ```
- */
- export function defineRegistry<
- C extends Catalog,
- TComponents extends Record<string, Component<BaseComponentProps<any>>>,
- >(
- _catalog: C,
- options: {
- /** Svelte components that accept BaseComponentProps */
- components?: TComponents;
- /** Action handlers */
- actions?: Record<string, DefineRegistryActionFn>;
- },
- ): DefineRegistryResult {
- const registry: ComponentRegistry = {};
- if (options.components) {
- for (const [name, componentFn] of Object.entries(options.components)) {
- registry[name] = (_, props) =>
- (componentFn as Component<BaseComponentProps<any>>)(_, {
- get props() {
- return props.element.props;
- },
- get children() {
- return props.children;
- },
- get emit() {
- return props.emit;
- },
- get on() {
- return props.on;
- },
- get bindings() {
- return props.bindings;
- },
- get loading() {
- return props.loading;
- },
- });
- }
- }
- // Build action helpers
- const actionMap = options.actions
- ? (Object.entries(options.actions) as Array<
- [string, DefineRegistryActionFn]
- >)
- : [];
- const handlers = (
- getSetState: () => SetState | undefined,
- getState: () => StateModel,
- ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {
- const result: Record<
- string,
- (params: Record<string, unknown>) => Promise<void>
- > = {};
- for (const [name, actionFn] of actionMap) {
- result[name] = async (params) => {
- const setState = getSetState();
- const state = getState();
- if (setState) {
- await actionFn(params, setState, state);
- }
- };
- }
- return result;
- };
- const executeAction = async (
- actionName: string,
- params: Record<string, unknown> | undefined,
- setState: SetState,
- state: StateModel = {},
- ): Promise<void> => {
- const entry = actionMap.find(([name]) => name === actionName);
- if (entry) {
- await entry[1](params, setState, state);
- } else {
- console.warn(`Unknown action: ${actionName}`);
- }
- };
- return { registry, handlers, executeAction };
- }
- // ============================================================================
- // createRenderer
- // ============================================================================
- /**
- * Props for renderers created with createRenderer
- */
- export interface CreateRendererProps {
- spec: Spec | null;
- store?: StateStore;
- state?: Record<string, unknown>;
- onAction?: (actionName: string, params?: Record<string, unknown>) => void;
- onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
- /** Named functions for `$computed` expressions in props */
- functions?: Record<string, ComputedFunction>;
- /** Custom directives for user-defined `$`-prefixed dynamic values */
- directives?: DirectiveDefinition[];
- loading?: boolean;
- fallback?: Component;
- }
- /**
- * Component map type — maps component names to Svelte components
- */
- export type ComponentMap<
- TComponents extends Record<string, { props: unknown }>,
- > = {
- [K in keyof TComponents]: Component<any, any, string>;
- };
- /**
- * Create a renderer from a catalog
- *
- * @example
- * ```typescript
- * const DashboardRenderer = createRenderer(dashboardCatalog, {
- * Card,
- * Metric,
- * });
- *
- * // Usage in template
- * <DashboardRenderer spec={aiGeneratedSpec} {state} />
- * ```
- */
- export function createRenderer<
- TDef extends SchemaDefinition,
- TCatalog extends { components: Record<string, { props: unknown }> },
- >(
- _catalog: Catalog<TDef, TCatalog>,
- components: ComponentMap<TCatalog["components"]>,
- ): Component<CreateRendererProps> {
- const registry: ComponentRegistry =
- components as unknown as ComponentRegistry;
- return (_, props: CreateRendererProps) =>
- CatalogRenderer(_, {
- registry,
- get spec() {
- return props.spec;
- },
- get store() {
- return props.store;
- },
- get state() {
- return props.state;
- },
- get onAction() {
- return props.onAction;
- },
- get onStateChange() {
- return props.onStateChange;
- },
- get functions() {
- return props.functions;
- },
- get directives() {
- return props.directives;
- },
- get loading() {
- return props.loading;
- },
- get fallback() {
- return props.fallback;
- },
- });
- }
|