@dxos/app-framework 0.10.0 → 0.11.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.
- package/.storybook/main.mts +1 -1
- package/.storybook/preview.mts +2 -2
- package/CHANGELOG.md +93 -0
- package/TASKS.md +11 -0
- package/dist/lib/chunk-SurfaceManager.mjs +960 -0
- package/dist/lib/chunk-SurfaceManager.mjs.map +1 -0
- package/dist/lib/chunk-capability.mjs +15 -0
- package/dist/lib/chunk-capability.mjs.map +1 -0
- package/dist/lib/{browser/chunk-FJ4765WW.mjs → chunk-context.mjs} +4 -5
- package/dist/lib/chunk-context.mjs.map +1 -0
- package/dist/lib/chunk-history-tracker.mjs +160 -0
- package/dist/lib/chunk-history-tracker.mjs.map +1 -0
- package/dist/lib/chunk-plugin-manager.mjs +1390 -0
- package/dist/lib/chunk-plugin-manager.mjs.map +1 -0
- package/dist/lib/chunk-plugin-manifest.mjs +125 -0
- package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
- package/dist/lib/chunk-process-manager-capability.mjs +126 -0
- package/dist/lib/chunk-process-manager-capability.mjs.map +1 -0
- package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
- package/dist/lib/chunk-url-loader.mjs +347 -0
- package/dist/lib/chunk-url-loader.mjs.map +1 -0
- package/dist/lib/cli.mjs +78 -0
- package/dist/lib/cli.mjs.map +1 -0
- package/dist/lib/common/activation-events.mjs +35 -0
- package/dist/lib/common/activation-events.mjs.map +1 -0
- package/dist/lib/common/capabilities.mjs +184 -0
- package/dist/lib/common/capabilities.mjs.map +1 -0
- package/dist/lib/config.mjs +2 -0
- package/dist/lib/core/activation-event.mjs +55 -0
- package/dist/lib/core/activation-event.mjs.map +1 -0
- package/dist/lib/core/capability.mjs +189 -0
- package/dist/lib/core/capability.mjs.map +1 -0
- package/dist/lib/core/plugin-manager.mjs +2 -0
- package/dist/lib/core/plugin.mjs +340 -0
- package/dist/lib/core/plugin.mjs.map +1 -0
- package/dist/lib/core/url-loader.mjs +2 -0
- package/dist/lib/index.mjs +134 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing/react.mjs +70 -0
- package/dist/lib/testing/react.mjs.map +1 -0
- package/dist/lib/testing.mjs +399 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/ui.mjs +636 -0
- package/dist/lib/ui.mjs.map +1 -0
- package/dist/lib/vite-plugin.mjs +986 -0
- package/dist/lib/vite-plugin.mjs.map +1 -0
- package/dist/plugin/node-esm/index.mjs +4 -3
- package/dist/plugin/node-esm/index.mjs.map +3 -3
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/common/Role.d.ts +31 -0
- package/dist/types/src/common/Role.d.ts.map +1 -0
- package/dist/types/src/common/Role.test.d.ts +2 -0
- package/dist/types/src/common/Role.test.d.ts.map +1 -0
- package/dist/types/src/common/capabilities.d.ts +11 -2
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/common/index.d.ts +1 -0
- package/dist/types/src/common/index.d.ts.map +1 -1
- package/dist/types/src/core/capability-manager.d.ts +1 -1
- package/dist/types/src/core/capability-manager.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +38 -5
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manager.d.ts +1 -1
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin.d.ts +14 -1
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/registry.d.ts +1 -1
- package/dist/types/src/core/registry.d.ts.map +1 -1
- package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
- package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
- package/dist/types/src/testing/StorybookErrorFallback.d.ts +15 -0
- package/dist/types/src/testing/StorybookErrorFallback.d.ts.map +1 -0
- 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/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/react.d.ts +4 -4
- 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 +6 -0
- package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
- package/dist/types/src/testing/withSurfaceDebug.d.ts +10 -0
- package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -0
- package/dist/types/src/ui/components/App/App.d.ts +0 -32
- package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/index.d.ts +1 -0
- package/dist/types/src/ui/components/App/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/loader.d.ts +35 -0
- package/dist/types/src/ui/components/App/loader.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts +26 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +15 -0
- package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts.map +1 -0
- package/dist/types/src/ui/components/HomeSection/index.d.ts +2 -0
- package/dist/types/src/ui/components/HomeSection/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts +14 -0
- package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts.map +1 -0
- package/dist/types/src/ui/components/NamePopover/index.d.ts +2 -0
- package/dist/types/src/ui/components/NamePopover/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +21 -9
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +4 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts +2 -0
- package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts +40 -0
- package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +23 -0
- package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts +11 -0
- package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts +72 -0
- package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts.map +1 -0
- package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +20 -25
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +56 -52
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/components/index.d.ts +2 -0
- package/dist/types/src/ui/components/index.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/moon.yml +25 -18
- package/package.json +58 -73
- package/src/common/Role.test.ts +41 -0
- package/src/common/Role.ts +54 -0
- package/src/common/capabilities.ts +17 -2
- package/src/common/index.ts +1 -0
- package/src/core/capability-manager.test.ts +1 -1
- package/src/core/capability-manager.ts +1 -1
- package/src/core/capability.ts +45 -6
- package/src/core/plugin-manager.test.ts +38 -3
- package/src/core/plugin-manager.ts +18 -1
- package/src/core/plugin.ts +16 -1
- package/src/core/registry.ts +1 -1
- package/src/plugin-process-manager/process-manager-capability.ts +25 -2
- package/src/testing/StorybookErrorFallback.tsx +40 -0
- package/src/testing/harness.ts +1 -1
- package/src/testing/index.ts +2 -0
- package/src/testing/react.test.tsx +2 -1
- package/src/testing/react.tsx +14 -8
- package/src/testing/withPluginManager.stories.tsx +16 -1
- package/src/testing/withPluginManager.tsx +13 -2
- package/src/testing/withSurfaceDebug.tsx +22 -0
- package/src/ui/components/App/App.tsx +7 -35
- package/src/ui/components/App/index.ts +1 -0
- package/src/ui/components/App/loader.ts +39 -0
- package/src/ui/components/HomeSection/HomeSection.stories.tsx +43 -0
- package/src/ui/components/HomeSection/HomeSection.tsx +69 -0
- package/src/ui/components/HomeSection/index.ts +5 -0
- package/src/ui/components/NamePopover/NamePopover.tsx +70 -0
- package/src/ui/components/NamePopover/index.ts +5 -0
- package/src/ui/components/Surface/DESIGN.md +136 -0
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +70 -41
- package/src/ui/components/Surface/SurfaceComponent.test.tsx +403 -0
- package/src/ui/components/Surface/SurfaceComponent.tsx +209 -189
- package/src/ui/components/Surface/SurfaceDebug.tsx +435 -0
- package/src/ui/components/Surface/SurfaceManager.ts +95 -0
- package/src/ui/components/Surface/SurfaceManagerContext.ts +23 -0
- package/src/ui/components/Surface/SurfaceMetrics.ts +202 -0
- package/src/ui/components/Surface/SurfaceProfilerContext.tsx +12 -2
- package/src/ui/components/Surface/index.ts +24 -22
- package/src/ui/components/Surface/types.test.ts +47 -54
- package/src/ui/components/Surface/types.ts +66 -91
- package/src/ui/components/index.ts +2 -0
- package/src/ui/hooks/useApp.tsx +7 -3
- package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +1 -1
- package/tsconfig.json +3 -0
- package/vite.config.ts +26 -0
- package/dist/lib/browser/capability-S44TE2H7.mjs +0 -35
- package/dist/lib/browser/capability-S44TE2H7.mjs.map +0 -7
- package/dist/lib/browser/chunk-3NQLTQCP.mjs +0 -47
- package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +0 -7
- package/dist/lib/browser/chunk-53HJHMS5.mjs +0 -7
- package/dist/lib/browser/chunk-53HJHMS5.mjs.map +0 -7
- package/dist/lib/browser/chunk-FJ4765WW.mjs.map +0 -7
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs +0 -505
- package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +0 -7
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs +0 -430
- package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +0 -7
- package/dist/lib/browser/chunk-NKVRSMFN.mjs +0 -95
- package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +0 -7
- package/dist/lib/browser/chunk-OO53M5UK.mjs +0 -260
- package/dist/lib/browser/chunk-OO53M5UK.mjs.map +0 -7
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs +0 -83
- package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +0 -7
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs +0 -12
- package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +0 -7
- package/dist/lib/browser/chunk-SYXIYT6T.mjs +0 -143
- package/dist/lib/browser/chunk-SYXIYT6T.mjs.map +0 -7
- package/dist/lib/browser/chunk-U5MESPM5.mjs +0 -603
- package/dist/lib/browser/chunk-U5MESPM5.mjs.map +0 -7
- package/dist/lib/browser/chunk-UGYCLOXE.mjs +0 -1471
- package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +0 -7
- package/dist/lib/browser/chunk-VF37YTXJ.mjs +0 -28
- package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +0 -7
- package/dist/lib/browser/chunk-YUVQIOTB.mjs +0 -60
- package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +0 -7
- package/dist/lib/browser/cli/index.mjs +0 -74
- package/dist/lib/browser/cli/index.mjs.map +0 -7
- package/dist/lib/browser/common/activation-events.mjs +0 -20
- package/dist/lib/browser/common/activation-events.mjs.map +0 -7
- package/dist/lib/browser/common/capabilities.mjs +0 -56
- package/dist/lib/browser/common/capabilities.mjs.map +0 -7
- package/dist/lib/browser/config/index.mjs +0 -8
- package/dist/lib/browser/config/index.mjs.map +0 -7
- package/dist/lib/browser/core/activation-event.mjs +0 -20
- package/dist/lib/browser/core/activation-event.mjs.map +0 -7
- package/dist/lib/browser/core/capability.mjs +0 -32
- package/dist/lib/browser/core/capability.mjs.map +0 -7
- package/dist/lib/browser/core/plugin-manager.mjs +0 -19
- package/dist/lib/browser/core/plugin-manager.mjs.map +0 -7
- package/dist/lib/browser/core/plugin.mjs +0 -51
- package/dist/lib/browser/core/plugin.mjs.map +0 -7
- package/dist/lib/browser/core/url-loader.mjs +0 -24
- package/dist/lib/browser/core/url-loader.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -98
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs +0 -132
- package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs.map +0 -7
- package/dist/lib/browser/testing/index.mjs +0 -359
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/browser/testing/react.mjs +0 -78
- package/dist/lib/browser/testing/react.mjs.map +0 -7
- package/dist/lib/browser/ui/index.mjs +0 -60
- package/dist/lib/browser/ui/index.mjs.map +0 -7
- package/dist/lib/node-esm/capability-3YRF77LV.mjs +0 -36
- package/dist/lib/node-esm/capability-3YRF77LV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +0 -9
- package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-37Z53PXZ.mjs +0 -10
- package/dist/lib/node-esm/chunk-37Z53PXZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs +0 -261
- package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs +0 -48
- package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-COHOWGGJ.mjs +0 -144
- package/dist/lib/node-esm/chunk-COHOWGGJ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs +0 -604
- package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs +0 -431
- package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs +0 -14
- package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs +0 -84
- package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs +0 -1472
- package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs +0 -506
- package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs +0 -29
- package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs +0 -62
- package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs +0 -96
- package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +0 -7
- package/dist/lib/node-esm/cli/index.mjs +0 -75
- package/dist/lib/node-esm/cli/index.mjs.map +0 -7
- package/dist/lib/node-esm/common/activation-events.mjs +0 -21
- package/dist/lib/node-esm/common/activation-events.mjs.map +0 -7
- package/dist/lib/node-esm/common/capabilities.mjs +0 -57
- package/dist/lib/node-esm/common/capabilities.mjs.map +0 -7
- package/dist/lib/node-esm/config/index.mjs +0 -9
- package/dist/lib/node-esm/config/index.mjs.map +0 -7
- package/dist/lib/node-esm/core/activation-event.mjs +0 -21
- package/dist/lib/node-esm/core/activation-event.mjs.map +0 -7
- package/dist/lib/node-esm/core/capability.mjs +0 -33
- package/dist/lib/node-esm/core/capability.mjs.map +0 -7
- package/dist/lib/node-esm/core/plugin-manager.mjs +0 -20
- package/dist/lib/node-esm/core/plugin-manager.mjs.map +0 -7
- package/dist/lib/node-esm/core/plugin.mjs +0 -52
- package/dist/lib/node-esm/core/plugin.mjs.map +0 -7
- package/dist/lib/node-esm/core/url-loader.mjs +0 -25
- package/dist/lib/node-esm/core/url-loader.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -99
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs +0 -133
- package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs.map +0 -7
- package/dist/lib/node-esm/testing/index.mjs +0 -360
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/testing/react.mjs +0 -79
- package/dist/lib/node-esm/testing/react.mjs.map +0 -7
- package/dist/lib/node-esm/ui/index.mjs +0 -61
- package/dist/lib/node-esm/ui/index.mjs.map +0 -7
- package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts +0 -11
- package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +0 -1
- package/src/ui/components/Surface/SurfaceInfo.tsx +0 -106
- package/vitest.config.ts +0 -14
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, {
|
|
6
|
+
type PropsWithChildren,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
createElement,
|
|
9
|
+
useEffect,
|
|
10
|
+
useLayoutEffect,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
useSyncExternalStore,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import { createPortal } from 'react-dom';
|
|
16
|
+
import { createRoot } from 'react-dom/client';
|
|
17
|
+
|
|
18
|
+
import { addEventListener, combine } from '@dxos/async';
|
|
19
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
|
+
|
|
21
|
+
import { type SurfaceContext } from './context';
|
|
22
|
+
import { type SurfaceMetric, surfaceMetricKey, surfaceMetrics } from './SurfaceMetrics';
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
interface Window {
|
|
26
|
+
__DX_DEBUG__?: boolean;
|
|
27
|
+
__DX__?: { surfaces: (component?: string) => HTMLElement[] };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEBUG_FLAG = '__DX_DEBUG__';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Custom element tag wrapping each React surface in debug builds.
|
|
35
|
+
*/
|
|
36
|
+
export const DX_SURFACE_TAG = 'dx-surface';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Installs the `window.__DX__` DevTools helper. `__DX__.surfaces(component?)` returns the mounted
|
|
40
|
+
* `<dx-surface>` elements (optionally filtered by `data-component`) — a console shortcut for the
|
|
41
|
+
* otherwise-verbose `document.querySelectorAll('dx-surface[data-component="…"]')`.
|
|
42
|
+
*/
|
|
43
|
+
const ensureDebugApi = (): void => {
|
|
44
|
+
if (typeof window === 'undefined') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
window.__DX__ = {
|
|
49
|
+
...window.__DX__,
|
|
50
|
+
surfaces: (component?: string) =>
|
|
51
|
+
Array.from(
|
|
52
|
+
document.querySelectorAll<HTMLElement>(
|
|
53
|
+
component ? `${DX_SURFACE_TAG}[data-component="${component}"]` : DX_SURFACE_TAG,
|
|
54
|
+
),
|
|
55
|
+
),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether surface debugging is enabled, via the `VITE_DEBUG` build flag or the
|
|
61
|
+
* `__DX_DEBUG__` runtime global. The runtime flag is toggleable without a
|
|
62
|
+
* rebuild and takes effect on the next render.
|
|
63
|
+
*/
|
|
64
|
+
export const isSurfaceDebugEnabled = (): boolean =>
|
|
65
|
+
Boolean(import.meta.env?.VITE_DEBUG) || (typeof window !== 'undefined' && DEBUG_FLAG in window);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the `<dx-surface>` wrapper is rendered. Enabled in any development build (or when
|
|
69
|
+
* `VITE_DEBUG` is set) so the wrapper's `data-*` attributes and `window.__DX__` helpers are available
|
|
70
|
+
* for DOM inspection independent of the runtime highlight flag. Production renders no wrapper.
|
|
71
|
+
*/
|
|
72
|
+
export const isSurfaceWrapperEnabled = (): boolean =>
|
|
73
|
+
Boolean(import.meta.env?.DEV) || Boolean(import.meta.env?.VITE_DEBUG);
|
|
74
|
+
|
|
75
|
+
// Notifies the overlay when the runtime highlight flag toggles: the overlay is a separate React root,
|
|
76
|
+
// so it does not re-render on a surface render and needs an explicit signal to redraw.
|
|
77
|
+
const flagListeners = new Set<() => void>();
|
|
78
|
+
const subscribeFlag = (listener: () => void): (() => void) => {
|
|
79
|
+
flagListeners.add(listener);
|
|
80
|
+
return () => {
|
|
81
|
+
flagListeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Toggles the `__DX_DEBUG__` runtime flag, which gates the visual highlight overlay (not the
|
|
87
|
+
* `<dx-surface>` wrapper). The overlay redraws immediately; surfaces pick it up on their next render.
|
|
88
|
+
*/
|
|
89
|
+
export const setSurfaceDebug = (enabled: boolean): void => {
|
|
90
|
+
if (typeof window === 'undefined') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (enabled) {
|
|
95
|
+
window[DEBUG_FLAG] = true;
|
|
96
|
+
} else {
|
|
97
|
+
delete window[DEBUG_FLAG];
|
|
98
|
+
}
|
|
99
|
+
for (const listener of flagListeners) {
|
|
100
|
+
listener();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
let elementRegistered = false;
|
|
105
|
+
|
|
106
|
+
const ensureSurfaceElement = (): void => {
|
|
107
|
+
if (elementRegistered || typeof customElements === 'undefined') {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
elementRegistered = true;
|
|
112
|
+
if (!customElements.get(DX_SURFACE_TAG)) {
|
|
113
|
+
// Defined lazily (not at module scope) so importing this module never evaluates
|
|
114
|
+
// `extends HTMLElement` in non-DOM environments (node tests / SSR).
|
|
115
|
+
// Zero-behavior element; `display: contents` keeps the debug wrapper layout-neutral
|
|
116
|
+
// so the debug DOM matches production (which renders no wrapper).
|
|
117
|
+
class DxSurfaceElement extends HTMLElement {
|
|
118
|
+
connectedCallback(): void {
|
|
119
|
+
this.style.display = 'contents';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
customElements.define(DX_SURFACE_TAG, DxSurfaceElement);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type InfoRef = { readonly current: SurfaceContext };
|
|
127
|
+
|
|
128
|
+
type DebugEntry = {
|
|
129
|
+
key: number;
|
|
130
|
+
element: HTMLElement;
|
|
131
|
+
infoRef: InfoRef;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Singleton registry of mounted debug surfaces. Centralizes overlay rendering so
|
|
136
|
+
* a single set of observers/listeners serves every surface, instead of each
|
|
137
|
+
* surface mounting its own portal and observers.
|
|
138
|
+
*/
|
|
139
|
+
class SurfaceDebugManager {
|
|
140
|
+
#entries = new Set<DebugEntry>();
|
|
141
|
+
#snapshot: DebugEntry[] = [];
|
|
142
|
+
#listeners = new Set<() => void>();
|
|
143
|
+
#nextKey = 0;
|
|
144
|
+
|
|
145
|
+
register(element: HTMLElement, infoRef: InfoRef): () => void {
|
|
146
|
+
const entry: DebugEntry = { key: this.#nextKey++, element, infoRef };
|
|
147
|
+
this.#entries.add(entry);
|
|
148
|
+
this.#update();
|
|
149
|
+
return () => {
|
|
150
|
+
this.#entries.delete(entry);
|
|
151
|
+
this.#update();
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
156
|
+
this.#listeners.add(listener);
|
|
157
|
+
return () => {
|
|
158
|
+
this.#listeners.delete(listener);
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
getSnapshot = (): DebugEntry[] => this.#snapshot;
|
|
163
|
+
|
|
164
|
+
#update(): void {
|
|
165
|
+
this.#snapshot = [...this.#entries];
|
|
166
|
+
for (const listener of this.#listeners) {
|
|
167
|
+
listener();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const manager = new SurfaceDebugManager();
|
|
173
|
+
|
|
174
|
+
let overlayMounted = false;
|
|
175
|
+
|
|
176
|
+
const ensureOverlay = (): void => {
|
|
177
|
+
if (overlayMounted || typeof document === 'undefined') {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
overlayMounted = true;
|
|
181
|
+
const root = document.createElement('div');
|
|
182
|
+
root.id = 'dx-surface-overlay';
|
|
183
|
+
document.body.appendChild(root);
|
|
184
|
+
createRoot(root).render(<SurfaceDebugOverlay />);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Measures a `display: contents` element (which has no box of its own) by unioning the border boxes
|
|
189
|
+
* of its rendered children. Deliberately uses each child's own `getBoundingClientRect` rather than a
|
|
190
|
+
* Range over all contents: a Range unions every descendant rect, so content overflowing an inner
|
|
191
|
+
* `overflow: auto` scroll container inflates the width past the surface's visible bounds. A direct
|
|
192
|
+
* child's box is clipped to its own border box, so the highlight stays tight to what is on screen.
|
|
193
|
+
*/
|
|
194
|
+
const measureContents = (element: HTMLElement): DOMRect | null => {
|
|
195
|
+
if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const boxes: DOMRect[] = [];
|
|
200
|
+
const collect = (node: Element): void => {
|
|
201
|
+
for (const child of node.children) {
|
|
202
|
+
// A nested `display: contents` child also has no box; descend to the elements that do.
|
|
203
|
+
if (getComputedStyle(child).display === 'contents') {
|
|
204
|
+
collect(child);
|
|
205
|
+
} else {
|
|
206
|
+
const rect = child.getBoundingClientRect();
|
|
207
|
+
if (rect.width > 0 || rect.height > 0) {
|
|
208
|
+
boxes.push(rect);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
collect(element);
|
|
214
|
+
|
|
215
|
+
if (boxes.length === 0) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let left = Infinity;
|
|
220
|
+
let top = Infinity;
|
|
221
|
+
let right = -Infinity;
|
|
222
|
+
let bottom = -Infinity;
|
|
223
|
+
for (const box of boxes) {
|
|
224
|
+
left = Math.min(left, box.left);
|
|
225
|
+
top = Math.min(top, box.top);
|
|
226
|
+
right = Math.max(right, box.right);
|
|
227
|
+
bottom = Math.max(bottom, box.bottom);
|
|
228
|
+
}
|
|
229
|
+
return new DOMRect(left, top, right - left, bottom - top);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const EMPTY_RECTS: ReadonlyMap<number, DOMRect> = new Map();
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Single overlay that draws boundary highlights for every registered surface.
|
|
236
|
+
* One ResizeObserver plus one scroll/resize listener serve all surfaces.
|
|
237
|
+
*/
|
|
238
|
+
const SurfaceDebugOverlay = (): ReactNode => {
|
|
239
|
+
const entries = useSyncExternalStore(manager.subscribe, manager.getSnapshot, manager.getSnapshot);
|
|
240
|
+
// The `__DX_DEBUG__` flag gates only this visual overlay; the `<dx-surface>` wrappers stay mounted.
|
|
241
|
+
const enabled = useSyncExternalStore(subscribeFlag, isSurfaceDebugEnabled, isSurfaceDebugEnabled);
|
|
242
|
+
const [rects, setRects] = useState<ReadonlyMap<number, DOMRect>>(EMPTY_RECTS);
|
|
243
|
+
|
|
244
|
+
useLayoutEffect(() => {
|
|
245
|
+
// Draw nothing (and install no listeners) while the flag is off or there are no surfaces.
|
|
246
|
+
if (!enabled || entries.length === 0) {
|
|
247
|
+
setRects(EMPTY_RECTS);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const measure = () => {
|
|
252
|
+
const next = new Map<number, DOMRect>();
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const rect = measureContents(entry.element);
|
|
255
|
+
if (rect) {
|
|
256
|
+
next.set(entry.key, rect);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
setRects(next);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Guard for environments without ResizeObserver (SSR / tests); scroll/resize still drive measurement.
|
|
263
|
+
const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : undefined;
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
// The element itself has no box (display: contents); observe its parent.
|
|
266
|
+
if (observer && entry.element.parentElement) {
|
|
267
|
+
observer.observe(entry.element.parentElement);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
measure();
|
|
271
|
+
|
|
272
|
+
return combine(addEventListener(window, 'scroll', measure, true), addEventListener(window, 'resize', measure), () =>
|
|
273
|
+
observer?.disconnect(),
|
|
274
|
+
);
|
|
275
|
+
}, [entries, enabled]);
|
|
276
|
+
|
|
277
|
+
if (!enabled) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return createPortal(
|
|
282
|
+
<>
|
|
283
|
+
{entries.map((entry) => {
|
|
284
|
+
const rect = rects.get(entry.key);
|
|
285
|
+
return rect ? <SurfaceHighlight key={entry.key} infoRef={entry.infoRef} rect={rect} /> : null;
|
|
286
|
+
})}
|
|
287
|
+
</>,
|
|
288
|
+
document.body,
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/** Subscribes to the metric for a single surface. */
|
|
293
|
+
const useSurfaceMetric = (surfaceId: string, role: string): SurfaceMetric | undefined => {
|
|
294
|
+
const all = useSyncExternalStore(surfaceMetrics.subscribe, surfaceMetrics.getSnapshot, surfaceMetrics.getSnapshot);
|
|
295
|
+
const key = surfaceMetricKey(surfaceId, role);
|
|
296
|
+
return all.find((metric) => metric.id === key);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const SurfaceHighlight = ({ infoRef, rect }: { infoRef: InfoRef; rect: DOMRect }): ReactNode => {
|
|
300
|
+
const [expand, setExpand] = useState(false);
|
|
301
|
+
const info = infoRef.current;
|
|
302
|
+
const metric = useSurfaceMetric(info.id ?? '', info.role);
|
|
303
|
+
// A surface with a likely problem (unstable data or a caught error) flags red.
|
|
304
|
+
const concern = !!metric && (metric.dataUnstable || metric.errors > 0);
|
|
305
|
+
return (
|
|
306
|
+
<div
|
|
307
|
+
className='z-[100] fixed flex flex-col-reverse scrollbar-none overflow-auto pointer-events-none'
|
|
308
|
+
style={{ top: rect.top, left: rect.left, width: rect.width, height: rect.height }}
|
|
309
|
+
>
|
|
310
|
+
{expand ? (
|
|
311
|
+
<div
|
|
312
|
+
className='absolute inset-0 border-2 border-rose-500 border-dotted overflow-auto pointer-events-auto'
|
|
313
|
+
onPointerDown={(ev) => ev.stopPropagation()}
|
|
314
|
+
onClick={(ev) => {
|
|
315
|
+
ev.stopPropagation();
|
|
316
|
+
setExpand(false);
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<div className='absolute left-1 right-1 bottom-1 max-h-[20rem] overflow-auto dx-card-surface ring-2 ring-separator rounded-sm opacity-90'>
|
|
320
|
+
<pre className='inline-block p-2 text-xs text-description font-mono font-thin'>
|
|
321
|
+
{JSON.stringify({ info, metric }, null, 2)}
|
|
322
|
+
</pre>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
) : (
|
|
326
|
+
<span
|
|
327
|
+
className={mx(
|
|
328
|
+
concern ? 'text-rose-500' : 'text-green-500',
|
|
329
|
+
'absolute right-2 bottom-1 flex items-center p-1 opacity-80 hover:opacity-100 text-sm cursor-pointer pointer-events-auto',
|
|
330
|
+
)}
|
|
331
|
+
title={metricSummary(info.id ?? '', metric)}
|
|
332
|
+
onPointerDown={(ev) => ev.stopPropagation()}
|
|
333
|
+
onClick={(ev) => {
|
|
334
|
+
ev.stopPropagation();
|
|
335
|
+
setExpand(true);
|
|
336
|
+
}}
|
|
337
|
+
>
|
|
338
|
+
{concern ? '⚠' : 'ⓘ'}
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const metricSummary = (surfaceId: string, metric: SurfaceMetric | undefined): string => {
|
|
346
|
+
if (!metric) {
|
|
347
|
+
return surfaceId;
|
|
348
|
+
}
|
|
349
|
+
const parts = [
|
|
350
|
+
surfaceId,
|
|
351
|
+
`dispatches=${metric.dispatches}`,
|
|
352
|
+
`candidates=${metric.candidates}${metric.truncated ? '(+truncated)' : ''}`,
|
|
353
|
+
`mounts=${metric.mounts}/unmounts=${metric.unmounts}`,
|
|
354
|
+
];
|
|
355
|
+
if (metric.dataUnstable) {
|
|
356
|
+
parts.push(`UNSTABLE data (churn=${metric.dataChurn})`);
|
|
357
|
+
}
|
|
358
|
+
if (metric.errors > 0) {
|
|
359
|
+
parts.push(`errors=${metric.errors}`);
|
|
360
|
+
}
|
|
361
|
+
return parts.join(' · ');
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Recovers the rendered surface component's name from the React fiber. Surfaces are registered as
|
|
366
|
+
* anonymous `component: (props) => <Real .../>` wrappers, so the static component carries no useful
|
|
367
|
+
* name; walking the fiber's child chain to the first component with a display name (as DevTools does)
|
|
368
|
+
* finds the real one. The wrapper's own inferred name is the property key ('component'), so skip it.
|
|
369
|
+
*/
|
|
370
|
+
const renderedComponentName = (element: HTMLElement): string | undefined => {
|
|
371
|
+
// React fiber internals are untyped; the fiber hangs off a `__reactFiber$<id>` own-property and its
|
|
372
|
+
// `child` chain is the rendered subtree.
|
|
373
|
+
const node = element as any;
|
|
374
|
+
const fiberKey = Object.keys(node).find((key) => key.startsWith('__reactFiber$'));
|
|
375
|
+
let fiber = fiberKey ? node[fiberKey]?.child : undefined;
|
|
376
|
+
while (fiber) {
|
|
377
|
+
const { type } = fiber;
|
|
378
|
+
if (typeof type === 'function') {
|
|
379
|
+
const name: string | undefined = type.displayName ?? type.name;
|
|
380
|
+
if (name && name !== 'component') {
|
|
381
|
+
return name;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
fiber = fiber.child;
|
|
385
|
+
}
|
|
386
|
+
return undefined;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Debug wrapper that mounts each React surface inside a `<dx-surface>` element
|
|
391
|
+
* and registers it with the centralized overlay. The element is named and
|
|
392
|
+
* inspectable in DevTools and queryable (`document.querySelectorAll('dx-surface')`).
|
|
393
|
+
*/
|
|
394
|
+
export const DebugSurface = ({ info, children }: PropsWithChildren<{ info: SurfaceContext }>): ReactNode => {
|
|
395
|
+
// The manager reads the latest info via this ref without re-registering on data change.
|
|
396
|
+
const infoRef = useRef(info);
|
|
397
|
+
infoRef.current = info;
|
|
398
|
+
const elementRef = useRef<HTMLElement>(null);
|
|
399
|
+
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
ensureSurfaceElement();
|
|
402
|
+
ensureOverlay();
|
|
403
|
+
ensureDebugApi();
|
|
404
|
+
const { id, role } = infoRef.current;
|
|
405
|
+
if (id) {
|
|
406
|
+
surfaceMetrics.recordMount(id, role);
|
|
407
|
+
}
|
|
408
|
+
const element = elementRef.current;
|
|
409
|
+
const unregister = element ? manager.register(element, infoRef) : undefined;
|
|
410
|
+
// Names the rendered component (`data-component`) once mounted — read from the fiber because the
|
|
411
|
+
// surface wrapper is anonymous, so no static name is available at render time.
|
|
412
|
+
const component = element ? renderedComponentName(element) : undefined;
|
|
413
|
+
if (element && component) {
|
|
414
|
+
element.setAttribute('data-component', component);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return () => {
|
|
418
|
+
if (id) {
|
|
419
|
+
surfaceMetrics.recordUnmount(id, role);
|
|
420
|
+
}
|
|
421
|
+
unregister?.();
|
|
422
|
+
};
|
|
423
|
+
}, []);
|
|
424
|
+
|
|
425
|
+
return createElement(
|
|
426
|
+
DX_SURFACE_TAG,
|
|
427
|
+
{
|
|
428
|
+
'className': 'contents',
|
|
429
|
+
'data-id': info.id,
|
|
430
|
+
'data-role': info.role,
|
|
431
|
+
'ref': elementRef,
|
|
432
|
+
},
|
|
433
|
+
children,
|
|
434
|
+
);
|
|
435
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom } from '@effect-atom/atom';
|
|
6
|
+
import * as Data from 'effect/Data';
|
|
7
|
+
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { Position } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { Capabilities } from '../../../common';
|
|
12
|
+
import { type CapabilityManager } from '../../../core';
|
|
13
|
+
import { type Definition, isValidLocalId } from './types';
|
|
14
|
+
|
|
15
|
+
const EMPTY_CANDIDATES: ReadonlyArray<Definition> = Data.array<Definition[]>([]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Groups definitions by role with each bucket pre-sorted by {@link Position}, so
|
|
19
|
+
* dispatch avoids a full scan and re-sort on every render. Pure helper — callers
|
|
20
|
+
* are responsible for filtering out invalid definitions before calling this.
|
|
21
|
+
*/
|
|
22
|
+
export const indexByRole = (definitions: Definition[]): Map<string, Definition[]> => {
|
|
23
|
+
const index = new Map<string, Definition[]>();
|
|
24
|
+
for (const definition of definitions) {
|
|
25
|
+
const roles = Array.isArray(definition.role) ? definition.role : [definition.role];
|
|
26
|
+
for (const role of roles) {
|
|
27
|
+
let bucket = index.get(role);
|
|
28
|
+
if (!bucket) {
|
|
29
|
+
bucket = [];
|
|
30
|
+
index.set(role, bucket);
|
|
31
|
+
}
|
|
32
|
+
bucket.push(definition);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const bucket of index.values()) {
|
|
36
|
+
bucket.sort(Position.compare);
|
|
37
|
+
}
|
|
38
|
+
return index;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Owns the per-manager surface memoization: one derived index atom plus a per-role
|
|
43
|
+
* family of candidate atoms. A single instance is provided via
|
|
44
|
+
* {@link SurfaceManagerProvider}, so instance identity does the per-manager keying
|
|
45
|
+
* (replacing module-level WeakMaps) and `Atom.family` does the per-role keying.
|
|
46
|
+
* Atom lifecycle is tied to the provider rather than module-global state.
|
|
47
|
+
*/
|
|
48
|
+
export class SurfaceManager {
|
|
49
|
+
readonly #capabilities: CapabilityManager.CapabilityManager;
|
|
50
|
+
|
|
51
|
+
// Role index (each bucket position-sorted); rebuilt once per contribution change.
|
|
52
|
+
readonly #index = Atom.make((get) => {
|
|
53
|
+
const definitions = get(this.#capabilities.atom(Capabilities.ReactSurface)).flat();
|
|
54
|
+
return indexByRole(this.#dropInvalid(definitions));
|
|
55
|
+
}).pipe(Atom.keepAlive);
|
|
56
|
+
|
|
57
|
+
// Per-role candidate atoms. `Data.array` gives the result structural equality, so a
|
|
58
|
+
// contribution to a different role recomputes to an equal value and is dropped —
|
|
59
|
+
// that role's subscribers never re-render.
|
|
60
|
+
readonly #candidates = Atom.family<string, Atom.Atom<ReadonlyArray<Definition>>>((role) =>
|
|
61
|
+
Atom.make((get) => {
|
|
62
|
+
const bucket = get(this.#index).get(role);
|
|
63
|
+
return bucket ? Data.array(bucket) : EMPTY_CANDIDATES;
|
|
64
|
+
}).pipe(Atom.keepAlive),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Ids already reported as invalid on this manager, so a persistently-malformed
|
|
68
|
+
// contribution warns once rather than on every index rebuild.
|
|
69
|
+
#warnedInvalidIds = new Set<string>();
|
|
70
|
+
|
|
71
|
+
constructor(capabilities: CapabilityManager.CapabilityManager) {
|
|
72
|
+
this.#capabilities = capabilities;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Derived atom yielding the (position-sorted) candidates for a single role. */
|
|
76
|
+
candidatesAtom(role: string): Atom.Atom<ReadonlyArray<Definition>> {
|
|
77
|
+
return this.#candidates(role);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Drops definitions with an invalid local id, warning once per id. */
|
|
81
|
+
#dropInvalid(definitions: Definition[]): Definition[] {
|
|
82
|
+
return definitions.filter((definition) => {
|
|
83
|
+
if (isValidLocalId(definition.id)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (!this.#warnedInvalidIds.has(definition.id)) {
|
|
87
|
+
this.#warnedInvalidIds.add(definition.id);
|
|
88
|
+
log.warn('dropping surface with invalid id; the final segment must be camelCase (no hyphens or underscores)', {
|
|
89
|
+
id: definition.id,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Context, createContext, useContext } from 'react';
|
|
6
|
+
|
|
7
|
+
import { raise } from '@dxos/debug';
|
|
8
|
+
|
|
9
|
+
import { type SurfaceManager } from './SurfaceManager';
|
|
10
|
+
|
|
11
|
+
const SurfaceManagerContext: Context<SurfaceManager | undefined> = createContext<SurfaceManager | undefined>(undefined);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the surface manager. Throws when rendered outside a {@link SurfaceManagerProvider};
|
|
15
|
+
* surfaces already hard-require the plugin-manager context, so there is no optional variant.
|
|
16
|
+
*/
|
|
17
|
+
export const useSurfaceManager = (): SurfaceManager =>
|
|
18
|
+
useContext(SurfaceManagerContext) ?? raise(new Error('Missing SurfaceManagerContext'));
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Context provider for a surface manager.
|
|
22
|
+
*/
|
|
23
|
+
export const SurfaceManagerProvider = SurfaceManagerContext.Provider;
|