@cratis/scene.react 4.8.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/SceneElementView.tsx +34 -5
  2. package/dist/cjs/SceneElementView.js +20 -5
  3. package/dist/cjs/SceneElementView.js.map +1 -1
  4. package/dist/cjs/core/coreComponents.js +4 -0
  5. package/dist/cjs/core/coreComponents.js.map +1 -1
  6. package/dist/cjs/core/coreDescriptors.js +3 -0
  7. package/dist/cjs/core/coreDescriptors.js.map +1 -1
  8. package/dist/cjs/index.js +19 -0
  9. package/dist/cjs/index.js.map +1 -1
  10. package/dist/cjs/navigation/resolveDestination.js +54 -0
  11. package/dist/cjs/navigation/resolveDestination.js.map +1 -0
  12. package/dist/cjs/packages/DesignTimeBundle.js +17 -0
  13. package/dist/cjs/packages/DesignTimeBundle.js.map +1 -0
  14. package/dist/cjs/packages/DesignTimeProvider.js +23 -0
  15. package/dist/cjs/packages/DesignTimeProvider.js.map +1 -0
  16. package/dist/cjs/packages/PackageHostConfiguration.js +71 -0
  17. package/dist/cjs/packages/PackageHostConfiguration.js.map +1 -0
  18. package/dist/cjs/packages/ScenePackageBundle.js +2 -0
  19. package/dist/cjs/packages/ScenePackageBundle.js.map +1 -1
  20. package/dist/cjs/renderer/AbortableQueryRunner.js +26 -0
  21. package/dist/cjs/renderer/AbortableQueryRunner.js.map +1 -0
  22. package/dist/cjs/renderer/BindingOutputContext.js +74 -0
  23. package/dist/cjs/renderer/BindingOutputContext.js.map +1 -0
  24. package/dist/cjs/renderer/InteractiveComponent.js +37 -2
  25. package/dist/cjs/renderer/InteractiveComponent.js.map +1 -1
  26. package/dist/cjs/renderer/RenderBindingScopeContext.js +77 -0
  27. package/dist/cjs/renderer/RenderBindingScopeContext.js.map +1 -0
  28. package/dist/esm/SceneElementView.d.ts +6 -2
  29. package/dist/esm/SceneElementView.d.ts.map +1 -1
  30. package/dist/esm/SceneElementView.js +20 -5
  31. package/dist/esm/SceneElementView.js.map +1 -1
  32. package/dist/esm/core/coreComponents.js +4 -0
  33. package/dist/esm/core/coreComponents.js.map +1 -1
  34. package/dist/esm/core/coreDescriptors.js +3 -0
  35. package/dist/esm/core/coreDescriptors.js.map +1 -1
  36. package/dist/esm/index.d.ts +1 -0
  37. package/dist/esm/index.d.ts.map +1 -1
  38. package/dist/esm/index.js +7 -0
  39. package/dist/esm/index.js.map +1 -1
  40. package/dist/esm/navigation/DestinationResolution.d.ts +10 -0
  41. package/dist/esm/navigation/DestinationResolution.d.ts.map +1 -0
  42. package/dist/esm/navigation/DestinationResolution.js +2 -0
  43. package/dist/esm/navigation/DestinationResolution.js.map +1 -0
  44. package/dist/esm/navigation/index.d.ts +3 -0
  45. package/dist/esm/navigation/index.d.ts.map +1 -0
  46. package/dist/esm/navigation/index.js +3 -0
  47. package/dist/esm/navigation/index.js.map +1 -0
  48. package/dist/esm/navigation/resolveDestination.d.ts +5 -0
  49. package/dist/esm/navigation/resolveDestination.d.ts.map +1 -0
  50. package/dist/esm/navigation/resolveDestination.js +52 -0
  51. package/dist/esm/navigation/resolveDestination.js.map +1 -0
  52. package/dist/esm/packages/DesignTimeAction.d.ts +22 -0
  53. package/dist/esm/packages/DesignTimeAction.d.ts.map +1 -0
  54. package/dist/esm/packages/DesignTimeAction.js +2 -0
  55. package/dist/esm/packages/DesignTimeAction.js.map +1 -0
  56. package/dist/esm/packages/DesignTimeBundle.d.ts +14 -0
  57. package/dist/esm/packages/DesignTimeBundle.d.ts.map +1 -0
  58. package/dist/esm/packages/DesignTimeBundle.js +15 -0
  59. package/dist/esm/packages/DesignTimeBundle.js.map +1 -0
  60. package/dist/esm/packages/DesignTimeComponentProps.d.ts +5 -0
  61. package/dist/esm/packages/DesignTimeComponentProps.d.ts.map +1 -0
  62. package/dist/esm/packages/DesignTimeComponentProps.js +2 -0
  63. package/dist/esm/packages/DesignTimeComponentProps.js.map +1 -0
  64. package/dist/esm/packages/DesignTimeContext.d.ts +18 -0
  65. package/dist/esm/packages/DesignTimeContext.d.ts.map +1 -0
  66. package/dist/esm/packages/DesignTimeContext.js +2 -0
  67. package/dist/esm/packages/DesignTimeContext.js.map +1 -0
  68. package/dist/esm/packages/DesignTimePropertyDisplayProps.d.ts +8 -0
  69. package/dist/esm/packages/DesignTimePropertyDisplayProps.d.ts.map +1 -0
  70. package/dist/esm/packages/DesignTimePropertyDisplayProps.js +2 -0
  71. package/dist/esm/packages/DesignTimePropertyDisplayProps.js.map +1 -0
  72. package/dist/esm/packages/DesignTimePropertyEditorProps.d.ts +9 -0
  73. package/dist/esm/packages/DesignTimePropertyEditorProps.d.ts.map +1 -0
  74. package/dist/esm/packages/DesignTimePropertyEditorProps.js +2 -0
  75. package/dist/esm/packages/DesignTimePropertyEditorProps.js.map +1 -0
  76. package/dist/esm/packages/DesignTimeProvider.d.ts +9 -0
  77. package/dist/esm/packages/DesignTimeProvider.d.ts.map +1 -0
  78. package/dist/esm/packages/DesignTimeProvider.js +20 -0
  79. package/dist/esm/packages/DesignTimeProvider.js.map +1 -0
  80. package/dist/esm/packages/PackageHostConfiguration.d.ts +25 -0
  81. package/dist/esm/packages/PackageHostConfiguration.d.ts.map +1 -0
  82. package/dist/esm/packages/PackageHostConfiguration.js +69 -0
  83. package/dist/esm/packages/PackageHostConfiguration.js.map +1 -0
  84. package/dist/esm/packages/ScenePackageBundle.d.ts +2 -0
  85. package/dist/esm/packages/ScenePackageBundle.d.ts.map +1 -1
  86. package/dist/esm/packages/ScenePackageBundle.js +2 -0
  87. package/dist/esm/packages/ScenePackageBundle.js.map +1 -1
  88. package/dist/esm/packages/index.d.ts +9 -0
  89. package/dist/esm/packages/index.d.ts.map +1 -1
  90. package/dist/esm/packages/index.js +9 -0
  91. package/dist/esm/packages/index.js.map +1 -1
  92. package/dist/esm/renderer/AbortableQueryRunner.d.ts +7 -0
  93. package/dist/esm/renderer/AbortableQueryRunner.d.ts.map +1 -0
  94. package/dist/esm/renderer/AbortableQueryRunner.js +24 -0
  95. package/dist/esm/renderer/AbortableQueryRunner.js.map +1 -0
  96. package/dist/esm/renderer/BindingOutputContext.d.ts +18 -0
  97. package/dist/esm/renderer/BindingOutputContext.d.ts.map +1 -0
  98. package/dist/esm/renderer/BindingOutputContext.js +70 -0
  99. package/dist/esm/renderer/BindingOutputContext.js.map +1 -0
  100. package/dist/esm/renderer/ComponentRegistry.d.ts +2 -0
  101. package/dist/esm/renderer/ComponentRegistry.d.ts.map +1 -1
  102. package/dist/esm/renderer/InteractiveComponent.d.ts.map +1 -1
  103. package/dist/esm/renderer/InteractiveComponent.js +38 -3
  104. package/dist/esm/renderer/InteractiveComponent.js.map +1 -1
  105. package/dist/esm/renderer/RenderBindingScopeContext.d.ts +14 -0
  106. package/dist/esm/renderer/RenderBindingScopeContext.d.ts.map +1 -0
  107. package/dist/esm/renderer/RenderBindingScopeContext.js +73 -0
  108. package/dist/esm/renderer/RenderBindingScopeContext.js.map +1 -0
  109. package/dist/esm/renderer/index.d.ts +3 -0
  110. package/dist/esm/renderer/index.d.ts.map +1 -1
  111. package/dist/esm/renderer/index.js +3 -0
  112. package/dist/esm/renderer/index.js.map +1 -1
  113. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  114. package/index.ts +1 -0
  115. package/navigation/DestinationResolution.ts +13 -0
  116. package/navigation/index.ts +5 -0
  117. package/navigation/resolveDestination.ts +50 -0
  118. package/package.json +5 -5
  119. package/packages/DesignTimeAction.ts +24 -0
  120. package/packages/DesignTimeBundle.ts +30 -0
  121. package/packages/DesignTimeComponentProps.ts +11 -0
  122. package/packages/DesignTimeContext.ts +32 -0
  123. package/packages/DesignTimePropertyDisplayProps.ts +14 -0
  124. package/packages/DesignTimePropertyEditorProps.ts +15 -0
  125. package/packages/DesignTimeProvider.tsx +25 -0
  126. package/packages/PackageHostConfiguration.ts +98 -0
  127. package/packages/ScenePackageBundle.ts +5 -0
  128. package/packages/index.ts +9 -0
  129. package/renderer/AbortableQueryRunner.ts +27 -0
  130. package/renderer/BindingOutputContext.tsx +69 -0
  131. package/renderer/ComponentRegistry.ts +4 -0
  132. package/renderer/InteractiveComponent.tsx +30 -3
  133. package/renderer/RenderBindingScopeContext.tsx +73 -0
  134. package/renderer/index.ts +3 -0
package/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './SceneElementView';
5
5
  export * from './renderer';
6
6
  export * from './core';
7
7
  export * from './NavBar';
8
+ export * from './navigation';
8
9
  export * from './icons';
9
10
  export * from './packages';
10
11
  export * from './theme';
@@ -0,0 +1,13 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { DestinationKind } from '@cratis/scene.model';
5
+
6
+ export interface DestinationResolution {
7
+ kind: DestinationKind;
8
+ url?: string;
9
+ outlet?: string;
10
+ dialog?: string;
11
+ action: 'navigate' | 'openDialog' | 'openExternal';
12
+ diagnostics: string[];
13
+ }
@@ -0,0 +1,5 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ export * from './DestinationResolution';
5
+ export * from './resolveDestination';
@@ -0,0 +1,50 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { BindingScope, resolveBindingExpression } from '@cratis/scene.engine';
5
+ import { DestinationKind, DestinationReference } from '@cratis/scene.model';
6
+ import { DestinationResolution } from './DestinationResolution';
7
+
8
+ /**
9
+ * Resolves a destination into a renderer-neutral navigation action, deep link and target outlet/dialog.
10
+ */
11
+ export function resolveDestination(destination: DestinationReference, scope: BindingScope = {}): DestinationResolution {
12
+ const diagnostics: string[] = [];
13
+ const kind = destination.kind ?? (destination.dialog ? DestinationKind.Dialog : DestinationKind.Outlet);
14
+ const parameters = Object.entries(destination.routeParameterBindings ?? {})
15
+ .map(([name, binding]) => [name, String(resolveBindingExpression(binding, scope) ?? '')] as const);
16
+ const base = destination.route ?? identityRoute(destination);
17
+ if (!base && kind !== DestinationKind.Dialog) diagnostics.push('Destination has no routeable screen, stable identity or route.');
18
+
19
+ const url = base ? appendParameters(base, parameters) : undefined;
20
+ return {
21
+ kind,
22
+ url,
23
+ outlet: destination.outlet,
24
+ dialog: destination.dialog,
25
+ action: kind === DestinationKind.Dialog ? 'openDialog' : kind === DestinationKind.External ? 'openExternal' : 'navigate',
26
+ diagnostics,
27
+ };
28
+ }
29
+
30
+ function identityRoute(destination: DestinationReference): string | undefined {
31
+ if (destination.screen) return destination.screen;
32
+ const parts = [destination.module, destination.feature, destination.slice].filter((part): part is string => typeof part === 'string' && part.length > 0);
33
+ return parts.length ? parts.join('/') : undefined;
34
+ }
35
+
36
+ function appendParameters(route: string, parameters: readonly (readonly [string, string])[]): string {
37
+ if (!parameters.length) return route;
38
+ let replaced = route;
39
+ const unused: string[] = [];
40
+ for (const [name, value] of parameters) {
41
+ const marker = `{${name}}`;
42
+ if (replaced.includes(marker)) {
43
+ replaced = replaced.split(marker).join(encodeURIComponent(value));
44
+ } else {
45
+ unused.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`);
46
+ }
47
+ }
48
+
49
+ return unused.length ? `${replaced}?${unused.join('&')}` : replaced;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.react",
3
- "version": "4.8.0",
3
+ "version": "4.10.0",
4
4
  "description": "The React renderer implementing Scene.Engine's Renderer contract against real React/DOM.",
5
5
  "keywords": [
6
6
  "cratis",
@@ -51,12 +51,12 @@
51
51
  "build-storybook": "storybook build"
52
52
  },
53
53
  "dependencies": {
54
- "@cratis/scene.engine": "4.8.0",
55
- "@cratis/scene.model": "4.8.0"
54
+ "@cratis/scene.engine": "4.10.0",
55
+ "@cratis/scene.model": "4.10.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@cratis/scene.engine": "4.8.0",
59
- "@cratis/scene.model": "4.8.0",
58
+ "@cratis/scene.engine": "4.10.0",
59
+ "@cratis/scene.model": "4.10.0",
60
60
  "@storybook/addon-links": "^10.6.1",
61
61
  "@storybook/react": "^10.6.1",
62
62
  "@storybook/react-vite": "^10.6.1",
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { DesignTimeActionDescriptor, SceneEdit } from '@cratis/scene.model';
5
+ import { DesignTimeContext } from './DesignTimeContext';
6
+
7
+ export interface DesignTimeActionContext extends DesignTimeContext {
8
+ commandMetadata?: { properties: { name: string; type: string; label?: string }[] };
9
+ }
10
+
11
+ export interface DesignTimeActionResult {
12
+ edits: SceneEdit[];
13
+ diagnostics: string[];
14
+ }
15
+
16
+ /**
17
+ * Executable package-owned design-time action. Hosts apply the returned edit batch through their own undo/redo.
18
+ */
19
+ export interface DesignTimeAction {
20
+ descriptor: DesignTimeActionDescriptor;
21
+ isVisible(context: DesignTimeActionContext): boolean;
22
+ isEnabled(context: DesignTimeActionContext): boolean;
23
+ execute(context: DesignTimeActionContext): DesignTimeActionResult;
24
+ }
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ComponentType } from 'react';
5
+ import { DesignTimeAction } from './DesignTimeAction';
6
+ import { DesignTimeComponentProps } from './DesignTimeComponentProps';
7
+ import { DesignTimePropertyDisplayProps } from './DesignTimePropertyDisplayProps';
8
+ import { DesignTimePropertyEditorProps } from './DesignTimePropertyEditorProps';
9
+
10
+ /**
11
+ * Optional React-only design-time surface a package may publish beside its runtime bundle.
12
+ */
13
+ export interface DesignTimeBundle {
14
+ previews?: Record<string, ComponentType<DesignTimeComponentProps>>;
15
+ designers?: Record<string, ComponentType<DesignTimeComponentProps>>;
16
+ propertyEditors?: Record<string, ComponentType<DesignTimePropertyEditorProps>>;
17
+ propertyDisplays?: Record<string, ComponentType<DesignTimePropertyDisplayProps>>;
18
+ actions?: Record<string, DesignTimeAction>;
19
+ }
20
+
21
+ export function validateDesignTimeBundle(bundle: DesignTimeBundle | undefined): string[] {
22
+ if (!bundle) return [];
23
+ const problems: string[] = [];
24
+ for (const [name, action] of Object.entries(bundle.actions ?? {})) {
25
+ if (action.descriptor.id !== name) {
26
+ problems.push(`registers design-time action '${name}' with descriptor id '${action.descriptor.id}'`);
27
+ }
28
+ }
29
+ return problems;
30
+ }
@@ -0,0 +1,11 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { DesignTimeContext } from './DesignTimeContext';
5
+
6
+ /**
7
+ * Props for a package-provided preview or full component designer.
8
+ */
9
+ export interface DesignTimeComponentProps {
10
+ context: DesignTimeContext;
11
+ }
@@ -0,0 +1,32 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import {
5
+ ComponentDescriptor,
6
+ EffectiveConfiguration,
7
+ ExternalComponent,
8
+ SceneEdit,
9
+ SceneElement,
10
+ Screen,
11
+ UiProfile,
12
+ } from '@cratis/scene.model';
13
+ import { BindingDiagnostic } from '@cratis/scene.engine';
14
+ import { ScenePackageBundle } from './ScenePackageBundle';
15
+
16
+ /**
17
+ * Canonical Scene state and host capabilities a package-side design-time contribution may read and edit.
18
+ */
19
+ export interface DesignTimeContext {
20
+ element: ExternalComponent;
21
+ root: SceneElement;
22
+ screen?: Screen;
23
+ descriptor: ComponentDescriptor;
24
+ configuration?: EffectiveConfiguration;
25
+ profile: UiProfile;
26
+ bundles: ScenePackageBundle[];
27
+ permissions: Record<string, boolean>;
28
+ capabilities: Record<string, boolean>;
29
+ diagnostics: BindingDiagnostic[];
30
+ submitEdits(edits: SceneEdit[]): void;
31
+ submitAction(actionId: string, edits: SceneEdit[]): void;
32
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { PropertyDescriptor } from '@cratis/scene.model';
5
+ import { DesignTimeContext } from './DesignTimeContext';
6
+
7
+ /**
8
+ * Props for a package-provided read-only property display renderer.
9
+ */
10
+ export interface DesignTimePropertyDisplayProps {
11
+ context: DesignTimeContext;
12
+ property: PropertyDescriptor;
13
+ value: unknown;
14
+ }
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { PropertyDescriptor } from '@cratis/scene.model';
5
+ import { DesignTimeContext } from './DesignTimeContext';
6
+
7
+ /**
8
+ * Props for a package-provided property editor that writes canonical Scene edits.
9
+ */
10
+ export interface DesignTimePropertyEditorProps {
11
+ context: DesignTimeContext;
12
+ property: PropertyDescriptor;
13
+ value: unknown;
14
+ setValue(value: unknown): void;
15
+ }
@@ -0,0 +1,25 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ReactNode, createContext, useContext } from 'react';
5
+ import { DesignTimeContext } from './DesignTimeContext';
6
+
7
+ const Context = createContext<DesignTimeContext | undefined>(undefined);
8
+
9
+ export interface DesignTimeProviderProps {
10
+ context: DesignTimeContext;
11
+ children: ReactNode;
12
+ }
13
+
14
+ /**
15
+ * Makes the generic package-side design-time context available to nested package components.
16
+ */
17
+ export function DesignTimeProvider({ context, children }: DesignTimeProviderProps) {
18
+ return <Context.Provider value={context}>{children}</Context.Provider>;
19
+ }
20
+
21
+ export function useDesignTimeContext(): DesignTimeContext {
22
+ const context = useContext(Context);
23
+ if (!context) throw new Error('No Scene design-time context is available.');
24
+ return context;
25
+ }
@@ -0,0 +1,98 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { DialogTemplate, Layout, ScreenTemplate, Theme, UiProfile } from '@cratis/scene.model';
5
+ import { resolvePackageDependencies } from '@cratis/scene.engine';
6
+ import { IconAdapterRegistry, createIconAdapterRegistry } from '../icons/IconAdapterRegistry';
7
+ import { ScenePackageBundle, mergePackageRegistries, validatePackageBundle } from './ScenePackageBundle';
8
+
9
+ export interface PackageHostPolicy {
10
+ allowExecutableImports: boolean;
11
+ allowNetworkAssets: boolean;
12
+ }
13
+
14
+ export interface PackageHostConfiguration {
15
+ bundles: ScenePackageBundle[];
16
+ profile: UiProfile;
17
+ policy: PackageHostPolicy;
18
+ }
19
+
20
+ export interface ResolvedPackageHost {
21
+ bundles: ScenePackageBundle[];
22
+ components: ReturnType<typeof mergePackageRegistries>;
23
+ layouts: Layout[];
24
+ screenTemplates: ScreenTemplate[];
25
+ dialogTemplates: DialogTemplate[];
26
+ themes: Theme[];
27
+ icons: IconAdapterRegistry;
28
+ assets: string[];
29
+ diagnostics: string[];
30
+ }
31
+
32
+ /**
33
+ * Resolves the approved package set a standalone or embedded React host uses.
34
+ */
35
+ export function resolvePackageHost(configuration: PackageHostConfiguration): ResolvedPackageHost {
36
+ const diagnostics: string[] = [];
37
+ const names = new Map<string, number>();
38
+ for (const bundle of configuration.bundles) {
39
+ names.set(bundle.manifest.name, (names.get(bundle.manifest.name) ?? 0) + 1);
40
+ }
41
+
42
+ for (const [name, count] of names) {
43
+ if (count > 1) diagnostics.push(`Package '${name}' is approved more than once; duplicate bundles are ambiguous`);
44
+ }
45
+
46
+ const byName = new Map(configuration.bundles.map(bundle => [bundle.manifest.name, bundle]));
47
+ const selection = resolvePackageDependencies(configuration.profile.packages, configuration.bundles.map(bundle => bundle.manifest));
48
+ for (const missing of selection.missing) diagnostics.push(`Package '${missing.package}' depends on missing package '${missing.dependsOn}'`);
49
+ for (const conflict of selection.versionConflicts) {
50
+ diagnostics.push(`Package '${conflict.package}' requires '${conflict.dependsOn}' ${conflict.requiredRange} but approved version is ${conflict.actualVersion}`);
51
+ }
52
+ for (const cycle of selection.cycles) diagnostics.push(`Package dependency cycle: ${cycle.join(' -> ')}`);
53
+
54
+ const bundles: ScenePackageBundle[] = [];
55
+ const runtimeSingletons = new Map<string, string>();
56
+ const assets: string[] = [];
57
+
58
+ for (const packageName of selection.packages) {
59
+ const bundle = byName.get(packageName);
60
+ if (!bundle) {
61
+ diagnostics.push(`UI profile asks for '${packageName}' but the host did not approve a bundle for it`);
62
+ continue;
63
+ }
64
+
65
+ diagnostics.push(...validatePackageBundle(bundle).map(problem => `${packageName}: ${problem}`));
66
+ if (!configuration.policy.allowExecutableImports && bundle.designTime) {
67
+ diagnostics.push(`Package '${packageName}' provides executable design-time contributions, but host policy forbids them`);
68
+ }
69
+
70
+ const packageAssets = bundle.manifest.assets ?? [];
71
+ if (!configuration.policy.allowNetworkAssets && packageAssets.some(asset => /^https?:\/\//.test(asset))) {
72
+ diagnostics.push(`Package '${packageName}' declares network assets, but host policy forbids them`);
73
+ }
74
+ assets.push(...packageAssets);
75
+
76
+ for (const dependency of bundle.manifest.runtimeSingletons ?? []) {
77
+ const owner = runtimeSingletons.get(dependency);
78
+ if (owner && owner !== packageName) {
79
+ diagnostics.push(`Runtime singleton '${dependency}' is provided by both '${owner}' and '${packageName}'`);
80
+ }
81
+ runtimeSingletons.set(dependency, packageName);
82
+ }
83
+
84
+ bundles.push(bundle);
85
+ }
86
+
87
+ return {
88
+ bundles,
89
+ components: mergePackageRegistries(bundles),
90
+ layouts: bundles.flatMap(bundle => bundle.layouts ?? []),
91
+ screenTemplates: bundles.flatMap(bundle => bundle.screenTemplates ?? []),
92
+ dialogTemplates: bundles.flatMap(bundle => bundle.dialogTemplates ?? []),
93
+ themes: bundles.flatMap(bundle => bundle.themes ?? []),
94
+ icons: createIconAdapterRegistry(bundles.map(bundle => bundle.iconLibrary?.adapter).filter(adapter => adapter !== undefined)),
95
+ assets,
96
+ diagnostics,
97
+ };
98
+ }
@@ -6,6 +6,7 @@ import { IconAdapterRegistry, createIconAdapterRegistry } from '../icons/IconAda
6
6
  import { IconLibraryBundle } from '../icons/IconLibraryBundle';
7
7
  import { ComponentRegistry } from '../renderer';
8
8
  import { ComponentsUiLibraryContract, validateComponentsUiLibraryContract } from './ComponentsUiLibraryContract';
9
+ import { DesignTimeBundle, validateDesignTimeBundle } from './DesignTimeBundle';
9
10
 
10
11
  /**
11
12
  * What a package actually ships to a React renderer, as opposed to what it *declares*.
@@ -77,6 +78,9 @@ export interface ScenePackageBundle {
77
78
  * authored; hosts check the loaded export with validateComponentsUiLibraryMapping before rendering.
78
79
  */
79
80
  componentsUiLibrary?: ComponentsUiLibraryContract;
81
+
82
+ /** Optional React-only design-time tooling. Runtime hosts can omit it entirely. */
83
+ designTime?: DesignTimeBundle;
80
84
  }
81
85
 
82
86
  /**
@@ -156,6 +160,7 @@ export function validatePackageBundle(bundle: ScenePackageBundle): string[] {
156
160
 
157
161
  problems.push(...validateIconLibrary(bundle));
158
162
  problems.push(...validateDescriptors(bundle));
163
+ problems.push(...validateDesignTimeBundle(bundle.designTime));
159
164
 
160
165
  return problems;
161
166
  }
package/packages/index.ts CHANGED
@@ -1,5 +1,14 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ export * from './DesignTimeAction';
5
+ export * from './DesignTimeBundle';
6
+ export * from './DesignTimeComponentProps';
7
+ export * from './DesignTimeContext';
8
+ export * from './DesignTimePropertyDisplayProps';
9
+ export * from './DesignTimePropertyEditorProps';
10
+ export * from './DesignTimeProvider';
11
+ export * from '../navigation';
12
+ export * from './PackageHostConfiguration';
4
13
  export * from './ScenePackageBundle';
5
14
  export * from './ComponentsUiLibraryContract';
@@ -0,0 +1,27 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ export type QueryPerformer = (query: string, argumentsValue: unknown, signal: AbortSignal) => Promise<unknown>;
5
+
6
+ /**
7
+ * Runs query bindings so a newer argument set aborts and ignores older work.
8
+ */
9
+ export class AbortableQueryRunner {
10
+ #controller?: AbortController;
11
+ #generation = 0;
12
+
13
+ async run(query: string, argumentsValue: unknown, performer: QueryPerformer): Promise<unknown | undefined> {
14
+ this.#controller?.abort();
15
+ const controller = new AbortController();
16
+ const generation = ++this.#generation;
17
+ this.#controller = controller;
18
+ const result = await performer(query, argumentsValue, controller.signal);
19
+ return generation === this.#generation && !controller.signal.aborted ? result : undefined;
20
+ }
21
+
22
+ clear(): void {
23
+ this.#controller?.abort();
24
+ this.#generation++;
25
+ this.#controller = undefined;
26
+ }
27
+ }
@@ -0,0 +1,69 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
5
+
6
+ export interface ComponentBindingOutputs {
7
+ setOutput(property: string, value: unknown): void;
8
+ clearOutput(property: string): void;
9
+ clearAll(): void;
10
+ }
11
+
12
+ export interface BindingOutputProviderValue {
13
+ outputs: Record<string, Record<string, unknown>>;
14
+ outputFor(elementId: string): ComponentBindingOutputs;
15
+ }
16
+
17
+ const emptyOutputs: ComponentBindingOutputs = {
18
+ setOutput: () => undefined,
19
+ clearOutput: () => undefined,
20
+ clearAll: () => undefined,
21
+ };
22
+
23
+ const Context = createContext<BindingOutputProviderValue | undefined>(undefined);
24
+
25
+ export interface BindingOutputProviderProps {
26
+ children: ReactNode;
27
+ onOutputsChanged?: (outputs: Record<string, Record<string, unknown>>) => void;
28
+ }
29
+
30
+ /**
31
+ * Tracks component output properties for typed component-property bindings and clears them with component lifecycle.
32
+ */
33
+ export function BindingOutputProvider({ children, onOutputsChanged }: BindingOutputProviderProps) {
34
+ const [outputs, setOutputs] = useState<Record<string, Record<string, unknown>>>({});
35
+
36
+ useEffect(() => {
37
+ onOutputsChanged?.(outputs);
38
+ }, [onOutputsChanged, outputs]);
39
+
40
+ const outputFor = useCallback((elementId: string): ComponentBindingOutputs => ({
41
+ setOutput: (property: string, propertyValue: unknown) => setOutputs(current => ({
42
+ ...current,
43
+ [elementId]: { ...(current[elementId] ?? {}), [property]: propertyValue },
44
+ })),
45
+ clearOutput: (property: string) => setOutputs(current => {
46
+ const elementOutputs = { ...(current[elementId] ?? {}) };
47
+ delete elementOutputs[property];
48
+ return { ...current, [elementId]: elementOutputs };
49
+ }),
50
+ clearAll: () => setOutputs(current => {
51
+ const next = { ...current };
52
+ delete next[elementId];
53
+ return next;
54
+ }),
55
+ }), []);
56
+
57
+ const value = useMemo<BindingOutputProviderValue>(() => ({ outputs, outputFor }), [outputs, outputFor]);
58
+
59
+ return <Context.Provider value={value}>{children}</Context.Provider>;
60
+ }
61
+
62
+ export function useBindingOutputs(elementId: string): ComponentBindingOutputs {
63
+ const outputFor = useContext(Context)?.outputFor;
64
+ return useMemo(() => outputFor?.(elementId) ?? emptyOutputs, [outputFor, elementId]);
65
+ }
66
+
67
+ export function useBindingOutputScope(): Record<string, Record<string, unknown>> {
68
+ return useContext(Context)?.outputs ?? {};
69
+ }
@@ -4,6 +4,7 @@
4
4
  import { ComponentType, ReactNode } from 'react';
5
5
  import { ExternalComponent } from '@cratis/scene.model';
6
6
  import { InteractionHandlers } from '../interactions';
7
+ import { ComponentBindingOutputs } from './BindingOutputContext';
7
8
 
8
9
  /**
9
10
  * The props every registered component receives.
@@ -21,6 +22,9 @@ export interface RegisteredComponentProps {
21
22
  * specification, or a gallery. Spreading it is safe either way.
22
23
  */
23
24
  interactions?: InteractionHandlers;
25
+
26
+ /** Component output property writer for typed element-to-element bindings. */
27
+ bindingOutputs?: ComponentBindingOutputs;
24
28
  }
25
29
 
26
30
  /**
@@ -1,10 +1,12 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
- import { ComponentType, ReactNode, createElement } from 'react';
5
- import { ExternalComponent } from '@cratis/scene.model';
4
+ import { ComponentType, ReactNode, createElement, useEffect, useMemo } from 'react';
5
+ import { BindingExpression, ExternalComponent } from '@cratis/scene.model';
6
6
  import { useInteractions } from '../interactions';
7
+ import { useBindingOutputs } from './BindingOutputContext';
7
8
  import { RegisteredComponentProps } from './ComponentRegistry';
9
+ import { useRenderBindingResolver } from './RenderBindingScopeContext';
8
10
 
9
11
  export interface InteractiveComponentProps {
10
12
  component: ComponentType<RegisteredComponentProps>;
@@ -21,5 +23,30 @@ export interface InteractiveComponentProps {
21
23
  */
22
24
  export function InteractiveComponent({ component, element, slots }: InteractiveComponentProps) {
23
25
  const interactions = useInteractions(element.id, element.behaviors);
24
- return createElement(component, { element, slots, interactions });
26
+ const bindingOutputs = useBindingOutputs(element.id);
27
+ const resolveBinding = useRenderBindingResolver();
28
+ const resolvedElement = useMemo(() => ({
29
+ ...element,
30
+ properties: resolveProperties(element.properties, resolveBinding),
31
+ }), [element, resolveBinding]);
32
+ useEffect(() => () => bindingOutputs.clearAll(), [element.id]);
33
+ return createElement(component, { element: resolvedElement, slots, interactions, bindingOutputs });
34
+ }
35
+
36
+ function resolveProperties(properties: Record<string, unknown>, resolveBinding: (binding: BindingExpression) => unknown): Record<string, unknown> {
37
+ return Object.fromEntries(Object.entries(properties).map(([name, value]) => [name, resolveValue(value, resolveBinding)]));
38
+ }
39
+
40
+ function resolveValue(value: unknown, resolveBinding: (binding: BindingExpression) => unknown): unknown {
41
+ if (isBindingExpression(value)) return resolveBinding(value);
42
+ if (Array.isArray(value)) return value.map(entry => resolveValue(entry, resolveBinding));
43
+ if (value !== null && typeof value === 'object') {
44
+ return Object.fromEntries(Object.entries(value as Record<string, unknown>).map(([name, entry]) => [name, resolveValue(entry, resolveBinding)]));
45
+ }
46
+
47
+ return value;
48
+ }
49
+
50
+ function isBindingExpression(value: unknown): value is BindingExpression {
51
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && typeof (value as Record<string, unknown>).path === 'string';
25
52
  }
@@ -0,0 +1,73 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ReactNode, createContext, useCallback, useContext, useMemo, useRef } from 'react';
5
+ import { BindingResolver, BindingScope, resolveBindingExpression } from '@cratis/scene.engine';
6
+ import { BindingExpression, BindingNullBehavior } from '@cratis/scene.model';
7
+ import { BindingOutputProvider, useBindingOutputScope } from './BindingOutputContext';
8
+
9
+ interface RenderBindingScopeValue {
10
+ resolveBinding: BindingResolver;
11
+ scope: BindingScope;
12
+ }
13
+
14
+ const Context = createContext<RenderBindingScopeValue | undefined>(undefined);
15
+
16
+ export interface RenderBindingScopeProviderProps {
17
+ children: ReactNode;
18
+ dataContext?: unknown;
19
+ queryResults?: Record<string, unknown>;
20
+ componentOutputs?: Record<string, Record<string, unknown>>;
21
+ resolveBinding?: BindingResolver;
22
+ onComponentOutputsChanged?: (outputs: Record<string, Record<string, unknown>>) => void;
23
+ }
24
+
25
+ /**
26
+ * Provides the inherited reactive binding scope for a rendered Scene tree.
27
+ */
28
+ export function RenderBindingScopeProvider(props: RenderBindingScopeProviderProps) {
29
+ const current = useContext(Context);
30
+ if (current) return <>{props.children}</>;
31
+
32
+ return (
33
+ <BindingOutputProvider onOutputsChanged={props.onComponentOutputsChanged}>
34
+ <RenderBindingScopeInner {...props} />
35
+ </BindingOutputProvider>
36
+ );
37
+ }
38
+
39
+ function RenderBindingScopeInner({ children, dataContext, queryResults, componentOutputs, resolveBinding }: RenderBindingScopeProviderProps) {
40
+ const outputs = useBindingOutputScope();
41
+ const previousValues = useRef(new Map<string, unknown>());
42
+ const scope = useMemo<BindingScope>(() => ({
43
+ dataContext,
44
+ queryResults,
45
+ componentOutputs: { ...(componentOutputs ?? {}), ...outputs },
46
+ }), [componentOutputs, dataContext, outputs, queryResults]);
47
+
48
+ const resolver = useCallback<BindingResolver>((binding: BindingExpression) => {
49
+ if (!binding.kind && resolveBinding) return resolveBinding(binding);
50
+
51
+ const key = JSON.stringify(binding);
52
+ const value = resolveBindingExpression(binding, scope);
53
+ if (binding.nullBehavior === BindingNullBehavior.Preserve && value === undefined) {
54
+ return previousValues.current.get(key);
55
+ }
56
+
57
+ if (value !== undefined) previousValues.current.set(key, value);
58
+ return value;
59
+ }, [resolveBinding, scope]);
60
+
61
+ const value = useMemo<RenderBindingScopeValue>(() => ({ resolveBinding: resolver, scope }), [resolver, scope]);
62
+ return <Context.Provider value={value}>{children}</Context.Provider>;
63
+ }
64
+
65
+ export function useRenderBindingResolver(): BindingResolver {
66
+ const context = useContext(Context);
67
+ if (!context) throw new Error('No Scene render binding scope is available.');
68
+ return context.resolveBinding;
69
+ }
70
+
71
+ export function useRenderBindingScope(): BindingScope {
72
+ return useContext(Context)?.scope ?? {};
73
+ }
package/renderer/index.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  // Copyright (c) Cratis. All rights reserved.
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
+ export * from './AbortableQueryRunner';
5
+ export * from './BindingOutputContext';
4
6
  export * from './ComponentRegistry';
7
+ export * from './RenderBindingScopeContext';
5
8
  export * from './UnresolvedComponent';
6
9
  export * from './createReactRenderer';
7
10
  export * from './InteractiveComponent';