@dxos/app-framework 0.9.0 → 0.9.1-staging.ee54ba693a
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.
- package/dist/lib/browser/{capability-BOPAKKWG.mjs → capability-S44TE2H7.mjs} +9 -9
- package/dist/lib/browser/{chunk-66IXTIVK.mjs → chunk-3NQLTQCP.mjs} +6 -7
- package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +7 -0
- package/dist/lib/browser/chunk-53HJHMS5.mjs +7 -0
- package/dist/lib/browser/chunk-53HJHMS5.mjs.map +7 -0
- package/dist/lib/browser/{chunk-Y66ELD4U.mjs → chunk-KRJ4KK2W.mjs} +36 -7
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +7 -0
- package/dist/lib/browser/{chunk-KZUDO43J.mjs → chunk-LUH4Y5F2.mjs} +31 -23
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +7 -0
- package/dist/lib/browser/{chunk-CO3XLNUX.mjs → chunk-NKVRSMFN.mjs} +18 -18
- package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-DC3WRPBV.mjs → chunk-OO53M5UK.mjs} +45 -12
- package/dist/lib/browser/chunk-OO53M5UK.mjs.map +7 -0
- package/dist/lib/browser/{chunk-IW44C7UL.mjs → chunk-OPTS3ZSN.mjs} +1 -1
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-HFLKXMT7.mjs → chunk-U5MESPM5.mjs} +61 -45
- package/dist/lib/browser/chunk-U5MESPM5.mjs.map +7 -0
- package/dist/lib/browser/{chunk-XHS5KDNX.mjs → chunk-UGYCLOXE.mjs} +25 -25
- package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5GY3YOEL.mjs → chunk-VF37YTXJ.mjs} +5 -5
- package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FO3IYSLV.mjs → chunk-YUVQIOTB.mjs} +29 -37
- package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +7 -0
- package/dist/lib/browser/cli/index.mjs +9 -9
- package/dist/lib/browser/cli/index.mjs.map +2 -2
- package/dist/lib/browser/common/activation-events.mjs +7 -7
- package/dist/lib/browser/common/capabilities.mjs +7 -7
- package/dist/lib/browser/config/index.mjs +8 -0
- package/dist/lib/browser/config/index.mjs.map +7 -0
- package/dist/lib/browser/core/activation-event.mjs +1 -1
- package/dist/lib/browser/core/capability.mjs +1 -1
- package/dist/lib/browser/core/plugin-manager.mjs +4 -4
- package/dist/lib/browser/core/plugin.mjs +12 -2
- package/dist/lib/browser/core/url-loader.mjs +3 -3
- package/dist/lib/browser/index.mjs +14 -10
- package/dist/lib/browser/index.mjs.map +1 -1
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs → process-manager-capability-QK4BMKMN.mjs} +9 -9
- package/dist/lib/browser/testing/index.mjs +15 -15
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/testing/react.mjs +11 -11
- package/dist/lib/browser/testing/react.mjs.map +3 -3
- package/dist/lib/browser/ui/index.mjs +18 -10
- package/dist/lib/node-esm/{capability-KZWRTX7R.mjs → capability-3YRF77LV.mjs} +9 -9
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +9 -0
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XIGPWXCQ.mjs → chunk-4SE7SX7N.mjs} +45 -12
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XOCUANHO.mjs → chunk-BMW6P6QA.mjs} +6 -7
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-A5CHGAZE.mjs → chunk-DRT75WGD.mjs} +61 -45
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-XGBUKRCK.mjs → chunk-FKMVORZN.mjs} +31 -23
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-CTKEZHKF.mjs → chunk-GLH4LGHZ.mjs} +1 -1
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-SX5NVETP.mjs → chunk-IL7O7IRX.mjs} +25 -25
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-FSVGKTF6.mjs → chunk-ND2HETHD.mjs} +36 -7
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-KFZEB6BV.mjs → chunk-TLKYZKPW.mjs} +5 -5
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-WK7OIQKI.mjs → chunk-VCPGX7WN.mjs} +29 -37
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-NULBHQYS.mjs → chunk-XSZMX53L.mjs} +18 -18
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +7 -0
- package/dist/lib/node-esm/cli/index.mjs +9 -9
- package/dist/lib/node-esm/cli/index.mjs.map +2 -2
- package/dist/lib/node-esm/common/activation-events.mjs +7 -7
- package/dist/lib/node-esm/common/capabilities.mjs +7 -7
- package/dist/lib/node-esm/config/index.mjs +9 -0
- package/dist/lib/node-esm/config/index.mjs.map +7 -0
- package/dist/lib/node-esm/core/activation-event.mjs +1 -1
- package/dist/lib/node-esm/core/capability.mjs +1 -1
- package/dist/lib/node-esm/core/plugin-manager.mjs +4 -4
- package/dist/lib/node-esm/core/plugin.mjs +12 -2
- package/dist/lib/node-esm/core/url-loader.mjs +3 -3
- package/dist/lib/node-esm/index.mjs +14 -10
- package/dist/lib/node-esm/index.mjs.map +1 -1
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs → process-manager-capability-5S7WZ2JR.mjs} +9 -9
- package/dist/lib/node-esm/testing/index.mjs +15 -15
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/testing/react.mjs +11 -11
- package/dist/lib/node-esm/testing/react.mjs.map +3 -3
- package/dist/lib/node-esm/ui/index.mjs +18 -10
- package/dist/plugin/node-esm/index.mjs +414 -81
- package/dist/plugin/node-esm/index.mjs.map +4 -4
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/cli/cli.d.ts +1 -1
- package/dist/types/src/common/activation-events.d.ts.map +1 -1
- package/dist/types/src/common/capabilities.d.ts +2 -2
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/config/index.d.ts +2 -0
- package/dist/types/src/config/index.d.ts.map +1 -0
- package/dist/types/src/core/activation-event.d.ts +9 -6
- package/dist/types/src/core/activation-event.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +9 -3
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/edge-registry-plugin-provider.d.ts +6 -11
- package/dist/types/src/core/edge-registry-plugin-provider.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manifest.d.ts +25 -7
- package/dist/types/src/core/plugin-manifest.d.ts.map +1 -1
- package/dist/types/src/core/plugin.d.ts +89 -91
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/registry.d.ts +17 -57
- package/dist/types/src/core/registry.d.ts.map +1 -1
- package/dist/types/src/core/url-loader.d.ts +2 -2
- package/dist/types/src/core/url-loader.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -3
- package/dist/types/src/plugin-process-manager/history/capability.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/meta.d.ts +32 -2
- package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -4
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
- package/dist/types/src/testing/harness.d.ts +1 -1
- package/dist/types/src/testing/harness.d.ts.map +1 -1
- package/dist/types/src/testing/react.d.ts +6 -5
- package/dist/types/src/testing/react.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
- package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/App.stories.d.ts +2 -2
- package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts +6 -0
- package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +6 -10
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/context.d.ts +6 -2
- package/dist/types/src/ui/components/Surface/context.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +11 -4
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +52 -72
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts +1 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts +17 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/composer/index.d.ts +11 -4
- package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/index.d.ts +1 -0
- package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/load.d.ts +16 -0
- package/dist/types/src/vite-plugin/load.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/manifest.d.ts +16 -4
- package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/docs/registry-spec.md +331 -0
- package/moon.yml +1 -0
- package/package.json +50 -39
- package/src/cli/cli.ts +2 -2
- package/src/common/activation-events.ts +3 -3
- package/src/common/capabilities.ts +18 -18
- package/src/config/index.ts +5 -0
- package/src/core/activation-event.ts +15 -8
- package/src/core/capability.ts +18 -6
- package/src/core/edge-registry-plugin-provider.ts +41 -42
- package/src/core/plugin-manager.test.ts +133 -137
- package/src/core/plugin-manager.ts +25 -19
- package/src/core/plugin-manifest.test.ts +5 -5
- package/src/core/plugin-manifest.ts +13 -10
- package/src/core/plugin.ts +58 -114
- package/src/core/registry.ts +20 -60
- package/src/core/url-loader.test.ts +2 -2
- package/src/core/url-loader.ts +23 -19
- package/src/index.ts +1 -0
- package/src/testing/harness.ts +5 -3
- package/src/testing/react.test.tsx +4 -2
- package/src/testing/react.tsx +10 -9
- package/src/testing/service.ts +2 -2
- package/src/testing/withPluginManager.stories.tsx +4 -2
- package/src/testing/withPluginManager.tsx +3 -1
- package/src/ui/components/App/App.stories.tsx +2 -2
- package/src/ui/components/PluginManager/PluginManagerProvider.ts +7 -0
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +22 -22
- package/src/ui/components/Surface/SurfaceComponent.tsx +90 -79
- package/src/ui/components/Surface/SurfaceProfilerContext.tsx +1 -1
- package/src/ui/components/Surface/context.ts +6 -4
- package/src/ui/components/Surface/index.ts +14 -3
- package/src/ui/components/Surface/types.test.ts +43 -51
- package/src/ui/components/Surface/types.ts +99 -64
- package/src/ui/hooks/useApp.test.tsx +1 -1
- package/src/ui/hooks/useApp.tsx +2 -2
- package/src/ui/hooks/useCapabilities.ts +49 -2
- package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +11 -11
- package/src/vite-plugin/boot-loader/loader.ts +3 -3
- package/src/vite-plugin/composer/index.ts +134 -61
- package/src/vite-plugin/import-map/index.ts +23 -7
- package/src/vite-plugin/index.ts +1 -0
- package/src/vite-plugin/load.ts +57 -0
- package/src/vite-plugin/manifest.test.ts +3 -3
- package/src/vite-plugin/manifest.ts +21 -10
- package/src/vite-plugin/packages.ts +0 -2
- package/tsconfig.json +5 -2
- package/dist/lib/browser/chunk-5GY3YOEL.mjs.map +0 -7
- package/dist/lib/browser/chunk-66IXTIVK.mjs.map +0 -7
- package/dist/lib/browser/chunk-CO3XLNUX.mjs.map +0 -7
- package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +0 -7
- package/dist/lib/browser/chunk-FO3IYSLV.mjs.map +0 -7
- package/dist/lib/browser/chunk-HFLKXMT7.mjs.map +0 -7
- package/dist/lib/browser/chunk-IW44C7UL.mjs.map +0 -7
- package/dist/lib/browser/chunk-KZUDO43J.mjs.map +0 -7
- package/dist/lib/browser/chunk-XHS5KDNX.mjs.map +0 -7
- package/dist/lib/browser/chunk-Y66ELD4U.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-A5CHGAZE.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-CTKEZHKF.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FSVGKTF6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-KFZEB6BV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-NULBHQYS.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-SX5NVETP.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-WK7OIQKI.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XGBUKRCK.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XOCUANHO.mjs.map +0 -7
- /package/dist/lib/browser/{capability-BOPAKKWG.mjs.map → capability-S44TE2H7.mjs.map} +0 -0
- /package/dist/lib/browser/{process-manager-capability-GXNUO7AR.mjs.map → process-manager-capability-QK4BMKMN.mjs.map} +0 -0
- /package/dist/lib/node-esm/{capability-KZWRTX7R.mjs.map → capability-3YRF77LV.mjs.map} +0 -0
- /package/dist/lib/node-esm/{process-manager-capability-TX5TA6YN.mjs.map → process-manager-capability-5S7WZ2JR.mjs.map} +0 -0
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
6
|
import React, {
|
|
7
|
+
type FC,
|
|
7
8
|
Fragment,
|
|
8
9
|
type NamedExoticComponent,
|
|
9
10
|
Profiler,
|
|
@@ -19,11 +20,11 @@ import React, {
|
|
|
19
20
|
import { log } from '@dxos/log';
|
|
20
21
|
import { ErrorBoundary } from '@dxos/react-error-boundary';
|
|
21
22
|
import { useDefaultValue } from '@dxos/react-hooks';
|
|
22
|
-
import {
|
|
23
|
+
import { Position } from '@dxos/util';
|
|
23
24
|
|
|
24
25
|
import { Capabilities } from '../../../common';
|
|
25
26
|
import { type CapabilityManager } from '../../../core';
|
|
26
|
-
import { usePluginManager } from '../PluginManager
|
|
27
|
+
import { usePluginManager } from '../PluginManager';
|
|
27
28
|
import { SurfaceContext } from './context';
|
|
28
29
|
import { SurfaceInfo } from './SurfaceInfo';
|
|
29
30
|
import { useSurfaceProfilerCallback } from './SurfaceProfilerContext';
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
type WebComponentDefinition,
|
|
37
38
|
} from './types';
|
|
38
39
|
|
|
39
|
-
const DEBUG = import.meta.env
|
|
40
|
+
const DEBUG = import.meta.env?.VITE_DEBUG;
|
|
40
41
|
|
|
41
42
|
const DEFAULT_PLACEHOLDER = <Fragment />;
|
|
42
43
|
|
|
@@ -44,78 +45,95 @@ const DEFAULT_PLACEHOLDER = <Fragment />;
|
|
|
44
45
|
* Wrapper component for rendering Web Component surfaces.
|
|
45
46
|
* Handles creation, prop setting, and cleanup of Web Components.
|
|
46
47
|
*/
|
|
48
|
+
type WebComponentWrapperProps = {
|
|
49
|
+
id?: string;
|
|
50
|
+
role: string;
|
|
51
|
+
data?: Record<string, any>;
|
|
52
|
+
limit?: number;
|
|
53
|
+
definition: WebComponentDefinition;
|
|
54
|
+
[key: string]: any;
|
|
55
|
+
};
|
|
56
|
+
|
|
47
57
|
const WebComponentWrapper = memo(
|
|
48
|
-
forwardRef<HTMLElement,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
58
|
+
forwardRef<HTMLElement, WebComponentWrapperProps>(({ id, role, data, limit, definition, ...rest }, forwardedRef) => {
|
|
59
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
const elementRef = useRef<HTMLElement | null>(null);
|
|
61
|
+
const propsRef = useRef({ id, role, data, limit, ...rest });
|
|
62
|
+
|
|
63
|
+
// Update props ref on every render
|
|
64
|
+
propsRef.current = { id, role, data, limit, ...rest };
|
|
65
|
+
|
|
66
|
+
// Create element only once
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!containerRef.current || elementRef.current) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
// Create the Web Component
|
|
73
|
+
const element = document.createElement(definition.tagName);
|
|
74
|
+
elementRef.current = element;
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
// Set initial properties on the Web Component
|
|
77
|
+
Object.assign(element, propsRef.current);
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
// Append to container
|
|
80
|
+
containerRef.current.appendChild(element);
|
|
72
81
|
|
|
73
|
-
|
|
82
|
+
// Setup ref forwarding if provided
|
|
83
|
+
if (typeof forwardedRef === 'function') {
|
|
84
|
+
forwardedRef(element);
|
|
85
|
+
} else if (forwardedRef) {
|
|
86
|
+
forwardedRef.current = element;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Cleanup on unmount to prevent memory leaks
|
|
90
|
+
return () => {
|
|
91
|
+
if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
|
|
92
|
+
containerRef.current.removeChild(elementRef.current);
|
|
93
|
+
}
|
|
74
94
|
if (typeof forwardedRef === 'function') {
|
|
75
|
-
forwardedRef(
|
|
95
|
+
forwardedRef(null);
|
|
76
96
|
} else if (forwardedRef) {
|
|
77
|
-
forwardedRef.current =
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Cleanup on unmount to prevent memory leaks
|
|
81
|
-
return () => {
|
|
82
|
-
if (elementRef.current && containerRef.current?.contains(elementRef.current)) {
|
|
83
|
-
containerRef.current.removeChild(elementRef.current);
|
|
84
|
-
}
|
|
85
|
-
if (typeof forwardedRef === 'function') {
|
|
86
|
-
forwardedRef(null);
|
|
87
|
-
} else if (forwardedRef) {
|
|
88
|
-
forwardedRef.current = null;
|
|
89
|
-
}
|
|
90
|
-
elementRef.current = null;
|
|
91
|
-
};
|
|
92
|
-
}, [definition.tagName, forwardedRef]);
|
|
93
|
-
|
|
94
|
-
// Update props on existing element without recreating it
|
|
95
|
-
// This runs on every render to ensure all props (including those in `rest`) are kept up to date
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
const element = elementRef.current;
|
|
98
|
-
if (!element) {
|
|
99
|
-
return;
|
|
97
|
+
forwardedRef.current = null;
|
|
100
98
|
}
|
|
99
|
+
elementRef.current = null;
|
|
100
|
+
};
|
|
101
|
+
}, [definition.tagName, forwardedRef]);
|
|
102
|
+
|
|
103
|
+
// Update props on existing element without recreating it
|
|
104
|
+
// This runs on every render to ensure all props (including those in `rest`) are kept up to date
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const element = elementRef.current;
|
|
107
|
+
if (!element) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
101
110
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
// Update properties on the existing Web Component
|
|
112
|
+
Object.assign(element, propsRef.current);
|
|
113
|
+
});
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
),
|
|
115
|
+
return <div ref={containerRef} />;
|
|
116
|
+
}),
|
|
109
117
|
);
|
|
110
118
|
|
|
111
119
|
WebComponentWrapper.displayName = 'WebComponentWrapper';
|
|
112
120
|
|
|
121
|
+
type SurfaceContextProviderProps = {
|
|
122
|
+
id: string;
|
|
123
|
+
role: string;
|
|
124
|
+
data?: Record<string, any>;
|
|
125
|
+
limit?: number;
|
|
126
|
+
fallback?: FC<{ error: Error; data?: any }>;
|
|
127
|
+
definition: Definition;
|
|
128
|
+
[key: string]: any;
|
|
129
|
+
};
|
|
130
|
+
|
|
113
131
|
/**
|
|
114
132
|
* Wrapper component that provides context for a surface.
|
|
115
133
|
*/
|
|
116
134
|
// TODO(burdon): Allow DebugPlugin to provide different fallback using react-ui ErrorFallback.
|
|
117
135
|
const SurfaceContextProvider = memo(
|
|
118
|
-
forwardRef<HTMLElement,
|
|
136
|
+
forwardRef<HTMLElement, SurfaceContextProviderProps>(
|
|
119
137
|
({ id, role, data, limit, fallback = ErrorFallback, definition, ...rest }, forwardedRef) => {
|
|
120
138
|
const contextValue = useMemo(() => ({ id, role, data }), [id, role, data]);
|
|
121
139
|
const onProfilerRender = useSurfaceProfilerCallback();
|
|
@@ -182,11 +200,13 @@ SurfaceContextProvider.displayName = 'SurfaceContextProvider';
|
|
|
182
200
|
|
|
183
201
|
/**
|
|
184
202
|
* A surface is a named region of the screen that can be populated by plugins.
|
|
203
|
+
* The `type` prop is a {@link RoleToken} that defines which region and its
|
|
204
|
+
* associated data contract.
|
|
185
205
|
*/
|
|
186
206
|
// TODO(burdon): Remove `ref` since relying on this would be error prone.
|
|
187
207
|
export const SurfaceComponent = memo(
|
|
188
|
-
forwardRef<HTMLElement,
|
|
189
|
-
({ id: _id,
|
|
208
|
+
forwardRef<HTMLElement, TypedProps<RoleToken<any>>>(
|
|
209
|
+
({ id: _id, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
|
|
190
210
|
const data = useDefaultValue(dataProp, () => ({}));
|
|
191
211
|
// TODO(wittjosiah): This will make all surfaces depend on a single signal.
|
|
192
212
|
// This isn't ideal because it means that any change to the data will cause all surfaces to re-render.
|
|
@@ -195,10 +215,10 @@ export const SurfaceComponent = memo(
|
|
|
195
215
|
// In the future, it would be nice to be able to bucket the surface contributions by role.
|
|
196
216
|
const surfaces = useSurfaces();
|
|
197
217
|
|
|
198
|
-
const effectiveRole =
|
|
218
|
+
const effectiveRole = type?.role;
|
|
199
219
|
if (effectiveRole == null) {
|
|
200
220
|
if (DEBUG) {
|
|
201
|
-
log.warn('Surface
|
|
221
|
+
log.warn('Surface is missing required `type` prop', { id: _id });
|
|
202
222
|
}
|
|
203
223
|
return null;
|
|
204
224
|
}
|
|
@@ -230,12 +250,12 @@ export const SurfaceComponent = memo(
|
|
|
230
250
|
},
|
|
231
251
|
),
|
|
232
252
|
) as (<TToken extends RoleToken<any>>(props: TypedProps<TToken> & RefAttributes<HTMLElement>) => React.ReactNode) &
|
|
233
|
-
NamedExoticComponent<
|
|
253
|
+
NamedExoticComponent<TypedProps<RoleToken<any>> & RefAttributes<HTMLElement>>;
|
|
234
254
|
|
|
235
255
|
SurfaceComponent.displayName = 'Surface';
|
|
236
256
|
|
|
237
257
|
// TODO(burdon): Make user facing, with telemetry.
|
|
238
|
-
const ErrorFallback = ({ error }:
|
|
258
|
+
const ErrorFallback = ({ error }: { error: Error }) => {
|
|
239
259
|
const { message } = error instanceof Error ? error : { message: String(error) };
|
|
240
260
|
return (
|
|
241
261
|
<div role='alert' data-testid='error-boundary-fallback'>
|
|
@@ -250,7 +270,7 @@ const findCandidates = (surfaces: Definition[], { role, data }: { role: string;
|
|
|
250
270
|
Array.isArray(definition.role) ? definition.role.includes(role) : definition.role === role,
|
|
251
271
|
)
|
|
252
272
|
.filter(({ filter }) => (filter ? filter(data ?? {}, role) : true))
|
|
253
|
-
.toSorted(
|
|
273
|
+
.toSorted(Position.compare);
|
|
254
274
|
};
|
|
255
275
|
|
|
256
276
|
/**
|
|
@@ -261,7 +281,7 @@ export const useSurfaces = () => {
|
|
|
261
281
|
const surfacesByModule = useAtomValue(manager.capabilities.atomByModule(Capabilities.ReactSurface));
|
|
262
282
|
return useMemo(() => {
|
|
263
283
|
const result: Definition[] = [];
|
|
264
|
-
for (const [
|
|
284
|
+
for (const [_moduleId, surfaces] of Object.entries(surfacesByModule)) {
|
|
265
285
|
for (const def of surfaces.flat()) {
|
|
266
286
|
result.push(def);
|
|
267
287
|
}
|
|
@@ -273,31 +293,22 @@ export const useSurfaces = () => {
|
|
|
273
293
|
/**
|
|
274
294
|
* @returns `true` if there is a contributed surface which matches the specified role & data, `false` otherwise.
|
|
275
295
|
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* declared contract (e.g. `AppSurface.Section` requires `attendableId`).
|
|
279
|
-
* - Legacy: pass a string `role` and `data` is untyped.
|
|
296
|
+
* Typed: pass a `type` role token and `data` is constrained to the token's
|
|
297
|
+
* declared contract (e.g. `AppSurface.Section` requires `attendableId`).
|
|
280
298
|
*/
|
|
281
299
|
export function isSurfaceAvailable<TToken extends RoleToken<any>>(
|
|
282
300
|
capabilityManager: CapabilityManager.CapabilityManager,
|
|
283
|
-
args: { type: TToken; data?: TokenData<TToken
|
|
284
|
-
): boolean;
|
|
285
|
-
export function isSurfaceAvailable(
|
|
286
|
-
capabilityManager: CapabilityManager.CapabilityManager,
|
|
287
|
-
args: Pick<Props, 'role' | 'data'> & { type?: undefined },
|
|
288
|
-
): boolean;
|
|
289
|
-
export function isSurfaceAvailable(
|
|
290
|
-
capabilityManager: CapabilityManager.CapabilityManager,
|
|
291
|
-
{ role, type, data }: { role?: string; type?: RoleToken<any>; data?: unknown },
|
|
301
|
+
args: { type: TToken; data?: TokenData<TToken> },
|
|
292
302
|
): boolean {
|
|
293
|
-
const effectiveRole =
|
|
303
|
+
const effectiveRole = args.type?.role;
|
|
294
304
|
if (effectiveRole == null) {
|
|
295
305
|
return false;
|
|
296
306
|
}
|
|
307
|
+
|
|
297
308
|
const surfaces = capabilityManager.getAll(Capabilities.ReactSurface);
|
|
298
309
|
const candidates = findCandidates(surfaces.flat(), {
|
|
299
310
|
role: effectiveRole,
|
|
300
|
-
data: data as Props['data'],
|
|
311
|
+
data: args.data as Props['data'],
|
|
301
312
|
});
|
|
302
313
|
return candidates.length > 0;
|
|
303
314
|
}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Context, createContext } from 'react';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
export type SurfaceContext = {
|
|
8
|
+
id?: string;
|
|
9
|
+
/** The resolved role NSID (e.g. `org.dxos.role.article`). */
|
|
10
|
+
role: string;
|
|
11
|
+
data?: Record<string, any>;
|
|
12
|
+
};
|
|
8
13
|
|
|
9
|
-
export type SurfaceContext = Pick<Props, 'id' | 'role' | 'data'>;
|
|
10
|
-
|
|
11
|
-
// TODO(burdon): Use @radix-ui/react-context
|
|
12
14
|
export const SurfaceContext: Context<SurfaceContext | undefined> = createContext<SurfaceContext | undefined>(undefined);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import type { DXN } from '@dxos/keys';
|
|
6
|
+
|
|
5
7
|
// TODO(wittjosiah): Cleanup to avoid re-naming.
|
|
6
8
|
import { SurfaceContext } from './context';
|
|
7
9
|
import { SurfaceComponent, isSurfaceAvailable } from './SurfaceComponent';
|
|
@@ -13,15 +15,16 @@ import {
|
|
|
13
15
|
useSurfaceProfilerStats,
|
|
14
16
|
} from './SurfaceProfilerContext';
|
|
15
17
|
import {
|
|
16
|
-
type Definition as SurfaceDefinition,
|
|
17
|
-
type RoleToken as SurfaceRoleToken,
|
|
18
18
|
type SurfaceBinding as SurfaceBindingType,
|
|
19
|
+
type Definition as SurfaceDefinition,
|
|
19
20
|
type SurfaceFilter as SurfaceFilterType,
|
|
21
|
+
type RoleToken as SurfaceRoleToken,
|
|
20
22
|
type TokenData as SurfaceTokenData,
|
|
21
23
|
type TypedProps as SurfaceTypedProps,
|
|
22
24
|
create as createSurface,
|
|
23
25
|
createWeb as createWebSurface,
|
|
24
26
|
isSurfaceFilter as isSurfaceFilterFn,
|
|
27
|
+
makeFilter as makeFilterFn,
|
|
25
28
|
makeType as makeTypeFn,
|
|
26
29
|
} from './types';
|
|
27
30
|
|
|
@@ -41,7 +44,15 @@ export namespace Surface {
|
|
|
41
44
|
export type Filter<TData> = SurfaceFilterType<TData>;
|
|
42
45
|
export type TokenData<T> = SurfaceTokenData<T>;
|
|
43
46
|
export type TypedProps<TToken extends SurfaceRoleToken<any>> = SurfaceTypedProps<TToken>;
|
|
44
|
-
|
|
47
|
+
|
|
48
|
+
/** Mints a typed role token. The NSID is validated at compile time via {@link DXN.Name}. */
|
|
49
|
+
export const makeType: {
|
|
50
|
+
<TData = unknown, T extends string = string>(
|
|
51
|
+
nsid: [DXN.Name<T>] extends [never] ? `Invalid NSID "${T}": final segment must be camelCase (no hyphens)` : T,
|
|
52
|
+
): SurfaceRoleToken<TData>;
|
|
53
|
+
} = makeTypeFn;
|
|
54
|
+
|
|
55
|
+
export const makeFilter = makeFilterFn;
|
|
45
56
|
export const isFilter = isSurfaceFilterFn;
|
|
46
57
|
|
|
47
58
|
export const ProfilerProvider = SurfaceProfilerProvider;
|
|
@@ -4,27 +4,39 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, test } from 'vitest';
|
|
6
6
|
|
|
7
|
+
import { Position } from '@dxos/util';
|
|
8
|
+
|
|
7
9
|
import { type CapabilityManager } from '../../../core';
|
|
8
10
|
import { isSurfaceAvailable } from './SurfaceComponent';
|
|
9
|
-
import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeType } from './types';
|
|
11
|
+
import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeFilter, makeType } from './types';
|
|
10
12
|
|
|
11
13
|
describe('Surface.makeType', () => {
|
|
12
|
-
test('creates a role token
|
|
13
|
-
const token = makeType<{ subject: string }>('test
|
|
14
|
-
expect(token.role).toBe('test
|
|
14
|
+
test('creates a role token with the given NSID', ({ expect }) => {
|
|
15
|
+
const token = makeType<{ subject: string }>('org.dxos.test.role.testRole');
|
|
16
|
+
expect(token.role).toBe('org.dxos.test.role.testRole');
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
test('tokens with the same role are independent objects (identity-by-role)', ({ expect }) => {
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
expect(
|
|
21
|
-
expect(
|
|
20
|
+
const tokenOne = makeType<{ x: number }>('org.dxos.test.role.shared');
|
|
21
|
+
const tokenTwo = makeType<{ x: number }>('org.dxos.test.role.shared');
|
|
22
|
+
expect(tokenOne).not.toBe(tokenTwo);
|
|
23
|
+
expect(tokenOne.role).toBe(tokenTwo.role);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('throws on invalid NSID at runtime (hyphenated final segment)', ({ expect }) => {
|
|
27
|
+
expect(() => makeType('org.dxos.test.role.bad-name' as any)).toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('throws on single-segment NSID', ({ expect }) => {
|
|
31
|
+
expect(() => makeType('article' as any)).toThrow();
|
|
22
32
|
});
|
|
23
33
|
});
|
|
24
34
|
|
|
25
35
|
describe('isSurfaceFilter', () => {
|
|
26
36
|
test('distinguishes filter objects from predicate functions', ({ expect }) => {
|
|
27
|
-
const filter: SurfaceFilter<Record<string, any>> = {
|
|
37
|
+
const filter: SurfaceFilter<Record<string, any>> = {
|
|
38
|
+
bindings: [{ role: 'org.dxos.test.role.r', guard: () => true }],
|
|
39
|
+
};
|
|
28
40
|
expect(isSurfaceFilter(filter)).toBe(true);
|
|
29
41
|
expect(isSurfaceFilter(() => true)).toBe(false);
|
|
30
42
|
expect(isSurfaceFilter({})).toBe(false);
|
|
@@ -33,61 +45,49 @@ describe('isSurfaceFilter', () => {
|
|
|
33
45
|
});
|
|
34
46
|
|
|
35
47
|
describe('create', () => {
|
|
36
|
-
test('accepts the legacy { role, filter } shape', ({ expect }) => {
|
|
37
|
-
const def = create({
|
|
38
|
-
id: 'legacy',
|
|
39
|
-
role: 'article',
|
|
40
|
-
filter: (data): data is { x: number } => typeof (data as any).x === 'number',
|
|
41
|
-
component: () => null,
|
|
42
|
-
});
|
|
43
|
-
expect(def.kind).toBe('react');
|
|
44
|
-
expect(def.role).toBe('article');
|
|
45
|
-
expect(def.filter!({ x: 1 })).toBe(true);
|
|
46
|
-
expect(def.filter!({})).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
48
|
test('expands a single-binding SurfaceFilter into a role string', ({ expect }) => {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
};
|
|
49
|
+
const token = makeType<Record<string, any>>('org.dxos.test.role.article');
|
|
50
|
+
const filter = makeFilter(token, (data) => data.subject === 'ok');
|
|
53
51
|
const def = create({ id: 'typedSingle', filter, component: () => null });
|
|
54
|
-
expect(def.role).toBe('article');
|
|
55
|
-
expect(def.filter!({ subject: 'ok' }, 'article')).toBe(true);
|
|
56
|
-
expect(def.filter!({ subject: 'no' }, 'article')).toBe(false);
|
|
52
|
+
expect(def.role).toBe('org.dxos.test.role.article');
|
|
53
|
+
expect(def.filter!({ subject: 'ok' }, 'org.dxos.test.role.article')).toBe(true);
|
|
54
|
+
expect(def.filter!({ subject: 'no' }, 'org.dxos.test.role.article')).toBe(false);
|
|
57
55
|
});
|
|
58
56
|
|
|
59
57
|
test('expands a multi-binding SurfaceFilter into a role array with role-scoped guards', ({ expect }) => {
|
|
58
|
+
const tokenA = makeType<Record<string, any>>('org.dxos.test.role.article');
|
|
59
|
+
const tokenB = makeType<Record<string, any>>('org.dxos.test.role.section');
|
|
60
60
|
const filter: SurfaceFilter<Record<string, any>> = {
|
|
61
61
|
bindings: [
|
|
62
|
-
{ role:
|
|
63
|
-
{ role:
|
|
62
|
+
{ role: tokenA.role, guard: (data) => (data as any).subject === 'a' },
|
|
63
|
+
{ role: tokenB.role, guard: (data) => (data as any).subject === 's' },
|
|
64
64
|
],
|
|
65
65
|
};
|
|
66
66
|
const def = create({ id: 'typedMulti', filter, component: () => null });
|
|
67
|
-
expect(def.role).toEqual([
|
|
67
|
+
expect(def.role).toEqual([tokenA.role, tokenB.role]);
|
|
68
68
|
// Role-specific guard.
|
|
69
|
-
expect(def.filter!({ subject: 'a' },
|
|
70
|
-
expect(def.filter!({ subject: 'a' },
|
|
71
|
-
expect(def.filter!({ subject: 's' },
|
|
72
|
-
expect(def.filter!({ subject: 's' },
|
|
69
|
+
expect(def.filter!({ subject: 'a' }, tokenA.role)).toBe(true);
|
|
70
|
+
expect(def.filter!({ subject: 'a' }, tokenB.role)).toBe(false);
|
|
71
|
+
expect(def.filter!({ subject: 's' }, tokenB.role)).toBe(true);
|
|
72
|
+
expect(def.filter!({ subject: 's' }, tokenA.role)).toBe(false);
|
|
73
73
|
// Without role, any binding may match.
|
|
74
74
|
expect(def.filter!({ subject: 'a' })).toBe(true);
|
|
75
75
|
expect(def.filter!({ subject: 'unknown' })).toBe(false);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test('passes position through untouched', ({ expect }) => {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
79
|
+
const token = makeType<Record<string, any>>('org.dxos.test.role.r');
|
|
80
|
+
const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: token.role, guard: () => true }] };
|
|
81
|
+
const def = create({ id: 'pos', filter, component: () => null, position: Position.last });
|
|
82
|
+
expect(def.position).toBe(Position.last);
|
|
82
83
|
});
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
describe('role token typing', () => {
|
|
86
87
|
test('makeType preserves TData through token use-sites', ({ expect }) => {
|
|
87
|
-
// Type-level smoke test: if TS compiles, we're fine.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
expect(token.role).toBe('numeric');
|
|
88
|
+
// Type-level smoke test: if TS compiles, we're fine.
|
|
89
|
+
const token: RoleToken<{ subject: number }> = makeType('org.dxos.test.role.numeric');
|
|
90
|
+
expect(token.role).toBe('org.dxos.test.role.numeric');
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
@@ -95,7 +95,7 @@ describe('isSurfaceAvailable typing', () => {
|
|
|
95
95
|
// These tests double as static assertions: the `@ts-expect-error` comments
|
|
96
96
|
// fail to compile if the surrounding expression typechecks, so they verify
|
|
97
97
|
// the typed overload narrows `data` to the token's declared contract.
|
|
98
|
-
const sectionToken = makeType<{ attendableId: string; subject: string }>('section');
|
|
98
|
+
const sectionToken = makeType<{ attendableId: string; subject: string }>('org.dxos.test.role.section');
|
|
99
99
|
const capabilityManager = { getAll: () => [] } as unknown as CapabilityManager.CapabilityManager;
|
|
100
100
|
|
|
101
101
|
test('typed overload accepts data matching the token contract', () => {
|
|
@@ -115,12 +115,4 @@ describe('isSurfaceAvailable typing', () => {
|
|
|
115
115
|
// @ts-expect-error — `attendableId` must be a string, not a number.
|
|
116
116
|
isSurfaceAvailable(capabilityManager, { type: sectionToken, data: { attendableId: 123, subject: 'x' } });
|
|
117
117
|
});
|
|
118
|
-
|
|
119
|
-
test('legacy overload accepts loose data when `role` is a string', () => {
|
|
120
|
-
// No error — legacy overload's `data` is untyped (`Record<string, unknown>`).
|
|
121
|
-
isSurfaceAvailable(capabilityManager, {
|
|
122
|
-
role: 'article',
|
|
123
|
-
data: { anything: 'goes' },
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
118
|
});
|