@dxos/app-framework 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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-BBBBAPDI.mjs → capability-Q5XRXRD2.mjs} +10 -10
- package/dist/lib/browser/{capability-OP63CD5N.mjs → capability-V7LR4LQN.mjs} +11 -11
- package/dist/lib/browser/capability-V7LR4LQN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-T3Y4AEKX.mjs → chunk-23D4SJUE.mjs} +3 -3
- package/dist/lib/browser/{chunk-T3Y4AEKX.mjs.map → chunk-23D4SJUE.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-2CKCJ6PN.mjs → chunk-3JWJXGLK.mjs} +1 -1
- package/dist/lib/browser/{chunk-2CKCJ6PN.mjs.map → chunk-3JWJXGLK.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-GX4TUNM6.mjs → chunk-3ZS2A3DN.mjs} +170 -226
- package/dist/lib/browser/chunk-3ZS2A3DN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-I34GF4NG.mjs → chunk-45CHLTBV.mjs} +2 -2
- package/dist/lib/browser/chunk-5LAIGWLU.mjs +467 -0
- package/dist/lib/browser/chunk-5LAIGWLU.mjs.map +7 -0
- package/dist/lib/browser/{chunk-QSXYHXCE.mjs → chunk-66IXTIVK.mjs} +1 -1
- package/dist/lib/browser/{chunk-QSXYHXCE.mjs.map → chunk-66IXTIVK.mjs.map} +2 -2
- package/dist/lib/browser/{chunk-TGX63LTL.mjs → chunk-FJ4765WW.mjs} +1 -1
- package/dist/lib/browser/{chunk-TGX63LTL.mjs.map → chunk-FJ4765WW.mjs.map} +2 -2
- package/dist/lib/browser/chunk-G7SDBRKH.mjs +1 -0
- package/dist/lib/browser/chunk-JXCBZSBJ.mjs +372 -0
- package/dist/lib/browser/chunk-JXCBZSBJ.mjs.map +7 -0
- package/dist/lib/browser/chunk-MX5DKEJH.mjs +584 -0
- package/dist/lib/browser/chunk-MX5DKEJH.mjs.map +7 -0
- package/dist/lib/browser/{chunk-JKWMHZP6.mjs → chunk-WBHCSOBW.mjs} +2 -2
- package/dist/lib/browser/chunk-WBHCSOBW.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FU4GAFUQ.mjs → chunk-Z55LVAGN.mjs} +80 -15
- package/dist/lib/browser/chunk-Z55LVAGN.mjs.map +7 -0
- package/dist/lib/browser/{chunk-F7FW2RK2.mjs → chunk-ZGJAZSNE.mjs} +7 -32
- package/dist/lib/browser/chunk-ZGJAZSNE.mjs.map +7 -0
- package/dist/lib/browser/cli/index.mjs +11 -27
- 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/core/activation-event.mjs +1 -1
- package/dist/lib/browser/core/capability.mjs +1 -1
- package/dist/lib/browser/core/plugin-manager.mjs +6 -4
- package/dist/lib/browser/core/plugin.mjs +10 -2
- package/dist/lib/browser/core/url-loader.mjs +13 -5
- package/dist/lib/browser/index.mjs +22 -18
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/{invoker-capability-H5PPENOC.mjs → invoker-capability-LNX4CGIV.mjs} +12 -11
- package/dist/lib/browser/invoker-capability-LNX4CGIV.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +144 -27
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/browser/testing/react.mjs +78 -0
- package/dist/lib/browser/testing/react.mjs.map +7 -0
- package/dist/lib/browser/ui/index.mjs +18 -14
- package/dist/lib/node-esm/{capability-AWBEMRYR.mjs → capability-EW5GJCI6.mjs} +10 -10
- package/dist/lib/node-esm/{capability-WFEG6CIZ.mjs → capability-YKBMMD53.mjs} +11 -11
- package/dist/lib/node-esm/capability-YKBMMD53.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs → chunk-37Z53PXZ.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-FKE4Z3D6.mjs.map → chunk-37Z53PXZ.mjs.map} +2 -2
- package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs → chunk-6XW6LET6.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-URWHJQT2.mjs → chunk-D347W3KO.mjs} +7 -32
- package/dist/lib/node-esm/chunk-D347W3KO.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-D5PO2WXX.mjs +373 -0
- package/dist/lib/node-esm/chunk-D5PO2WXX.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-ULUEXB7Q.mjs → chunk-HTBJU5FX.mjs} +80 -15
- package/dist/lib/node-esm/chunk-HTBJU5FX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-KM2F6GH6.mjs +468 -0
- package/dist/lib/node-esm/chunk-KM2F6GH6.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs → chunk-OZ7DZA5Z.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-BCEOLX47.mjs → chunk-Q7XBFII4.mjs} +170 -226
- package/dist/lib/node-esm/chunk-Q7XBFII4.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs → chunk-SBS2YMPT.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-VKHGNEDB.mjs.map → chunk-SBS2YMPT.mjs.map} +1 -1
- package/dist/lib/node-esm/{chunk-42KBWDE4.mjs → chunk-SDJ4B2LU.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-42KBWDE4.mjs.map → chunk-SDJ4B2LU.mjs.map} +1 -1
- package/dist/lib/node-esm/{chunk-G3RTFSNG.mjs → chunk-WFSRZKBP.mjs} +2 -2
- package/dist/lib/node-esm/chunk-WFSRZKBP.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-WKTLE7MG.mjs +585 -0
- package/dist/lib/node-esm/chunk-WKTLE7MG.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs → chunk-XOCUANHO.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-ZZ7CKK6W.mjs.map → chunk-XOCUANHO.mjs.map} +2 -2
- package/dist/lib/node-esm/cli/index.mjs +11 -27
- 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/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 +6 -4
- package/dist/lib/node-esm/core/plugin.mjs +10 -2
- package/dist/lib/node-esm/core/url-loader.mjs +13 -5
- package/dist/lib/node-esm/index.mjs +22 -18
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/{invoker-capability-S3ZA527J.mjs → invoker-capability-O4T5PHLA.mjs} +12 -11
- package/dist/lib/node-esm/invoker-capability-O4T5PHLA.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +144 -27
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/testing/react.mjs +79 -0
- package/dist/lib/node-esm/testing/react.mjs.map +7 -0
- package/dist/lib/node-esm/ui/index.mjs +18 -14
- package/dist/plugin/node-esm/index.mjs +480 -32
- package/dist/plugin/node-esm/index.mjs.map +4 -4
- package/dist/plugin/node-esm/meta.json +1 -1
- package/dist/types/src/common/capabilities.d.ts +2 -1
- package/dist/types/src/common/capabilities.d.ts.map +1 -1
- package/dist/types/src/common/operations.d.ts +1 -1
- package/dist/types/src/common/operations.d.ts.map +1 -1
- package/dist/types/src/core/activation-event.d.ts +4 -4
- package/dist/types/src/core/activation-event.d.ts.map +1 -1
- package/dist/types/src/core/capability-manager.d.ts.map +1 -1
- package/dist/types/src/core/capability.d.ts +2 -2
- package/dist/types/src/core/capability.d.ts.map +1 -1
- package/dist/types/src/core/index.d.ts +2 -0
- package/dist/types/src/core/index.d.ts.map +1 -1
- package/dist/types/src/core/plugin-asset-cache.d.ts +71 -0
- package/dist/types/src/core/plugin-asset-cache.d.ts.map +1 -0
- package/dist/types/src/core/plugin-manager.d.ts +51 -2
- package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
- package/dist/types/src/core/plugin-manifest.d.ts +76 -0
- package/dist/types/src/core/plugin-manifest.d.ts.map +1 -0
- package/dist/types/src/core/plugin-manifest.test.d.ts +2 -0
- package/dist/types/src/core/plugin-manifest.test.d.ts.map +1 -0
- package/dist/types/src/core/plugin.d.ts +107 -6
- package/dist/types/src/core/plugin.d.ts.map +1 -1
- package/dist/types/src/core/url-loader.d.ts +90 -3
- package/dist/types/src/core/url-loader.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/capability.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/errors.d.ts +6 -6
- package/dist/types/src/plugin-operation/history/errors.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/history-tracker.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/history-tracker.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/types.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/types.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/undo-mapping.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/undo-mapping.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/history/undo-registry.d.ts +1 -1
- package/dist/types/src/plugin-operation/history/undo-registry.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/invoker-capability.d.ts +1 -1
- package/dist/types/src/plugin-operation/invoker-capability.d.ts.map +1 -1
- package/dist/types/src/plugin-operation/testing.d.ts +2 -1
- package/dist/types/src/plugin-operation/testing.d.ts.map +1 -1
- package/dist/types/src/plugin-runtime/capability.d.ts +1 -1
- package/dist/types/src/plugin-runtime/capability.d.ts.map +1 -1
- package/dist/types/src/testing/harness.d.ts +67 -0
- package/dist/types/src/testing/harness.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/react.d.ts +27 -0
- package/dist/types/src/testing/react.d.ts.map +1 -0
- package/dist/types/src/testing/react.test.d.ts +2 -0
- package/dist/types/src/testing/react.test.d.ts.map +1 -0
- package/dist/types/src/testing/service.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.d.ts.map +1 -1
- package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +64 -0
- package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +1 -0
- package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +19 -0
- package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +1 -0
- package/dist/types/src/ui/components/Placeholder/index.d.ts +2 -0
- package/dist/types/src/ui/components/Placeholder/index.d.ts.map +1 -0
- package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +16 -4
- 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.map +1 -1
- package/dist/types/src/ui/components/Surface/index.d.ts +16 -6
- package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.d.ts +110 -9
- package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
- package/dist/types/src/ui/components/Surface/types.test.d.ts +2 -0
- package/dist/types/src/ui/components/Surface/types.test.d.ts.map +1 -0
- package/dist/types/src/ui/components/index.d.ts +1 -0
- package/dist/types/src/ui/components/index.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useApp.d.ts +29 -3
- package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useLoading.d.ts.map +1 -1
- package/dist/types/src/ui/hooks/useSettingsState.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +34 -0
- package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/boot-loader/index.d.ts +52 -0
- package/dist/types/src/vite-plugin/boot-loader/index.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/composer/index.d.ts +34 -0
- package/dist/types/src/vite-plugin/composer/index.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/import-map/index.d.ts +28 -0
- package/dist/types/src/vite-plugin/import-map/index.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/index.d.ts +4 -2
- package/dist/types/src/vite-plugin/index.d.ts.map +1 -1
- package/dist/types/src/vite-plugin/manifest.d.ts +37 -0
- package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/manifest.test.d.ts +2 -0
- package/dist/types/src/vite-plugin/manifest.test.d.ts.map +1 -0
- package/dist/types/src/vite-plugin/packages.d.ts +10 -4
- package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/moon.yml +1 -0
- package/package.json +33 -59
- package/src/common/capabilities.ts +2 -1
- package/src/common/operations.ts +1 -1
- package/src/core/capability.ts +1 -1
- package/src/core/index.ts +2 -0
- package/src/core/plugin-asset-cache.ts +60 -0
- package/src/core/plugin-manager.test.ts +246 -5
- package/src/core/plugin-manager.ts +167 -25
- package/src/core/plugin-manifest.test.ts +48 -0
- package/src/core/plugin-manifest.ts +102 -0
- package/src/core/plugin.ts +135 -10
- package/src/core/url-loader.test.ts +104 -5
- package/src/core/url-loader.ts +226 -37
- package/src/plugin-operation/OperationPlugin.ts +2 -2
- package/src/plugin-operation/history/capability.ts +1 -1
- package/src/plugin-operation/history/history-tracker.test.ts +2 -1
- package/src/plugin-operation/history/history-tracker.ts +1 -1
- package/src/plugin-operation/history/types.ts +1 -1
- package/src/plugin-operation/history/undo-mapping.ts +1 -1
- package/src/plugin-operation/history/undo-registry.ts +1 -1
- package/src/plugin-operation/invoker-capability.ts +2 -1
- package/src/plugin-operation/testing.ts +2 -1
- package/src/plugin-runtime/RuntimePlugin.ts +2 -2
- package/src/testing/harness.ts +229 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/react.test.tsx +48 -0
- package/src/testing/react.tsx +113 -0
- package/src/testing/withPluginManager.stories.tsx +1 -1
- package/src/ui/components/App/App.stories.tsx +1 -1
- package/src/ui/components/App/App.tsx +25 -2
- package/src/ui/components/Placeholder/Placeholder.stories.tsx +77 -0
- package/src/ui/components/Placeholder/Placeholder.tsx +155 -0
- package/src/ui/components/Placeholder/index.ts +5 -0
- package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +4 -2
- package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
- package/src/ui/components/Surface/SurfaceComponent.tsx +83 -46
- package/src/ui/components/Surface/index.ts +20 -1
- package/src/ui/components/Surface/types.test.ts +126 -0
- package/src/ui/components/Surface/types.ts +164 -12
- package/src/ui/components/index.ts +1 -0
- package/src/ui/hooks/useApp.tsx +165 -41
- package/src/ui/hooks/useLoading.tsx +14 -6
- package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +263 -0
- package/src/vite-plugin/boot-loader/boot-loader.css +294 -0
- package/src/vite-plugin/boot-loader/boot-loader.js +274 -0
- package/src/vite-plugin/boot-loader/index.ts +112 -0
- package/src/vite-plugin/composer/index.ts +277 -0
- package/src/vite-plugin/import-map/index.ts +524 -0
- package/src/vite-plugin/index.ts +6 -2
- package/src/vite-plugin/manifest.test.ts +24 -0
- package/src/vite-plugin/manifest.ts +50 -0
- package/src/vite-plugin/packages.ts +169 -10
- package/tsconfig.json +9 -0
- package/.swc/plugins/linux_x86_64_19.0.0/727453fb3a62f7f1d952a41e051ca8a6f88cadc45cee43c6a4d1aa45f9b75665.wasmer-v7 +0 -0
- package/dist/lib/browser/capability-OP63CD5N.mjs.map +0 -7
- package/dist/lib/browser/chunk-F7FW2RK2.mjs.map +0 -7
- package/dist/lib/browser/chunk-FU4GAFUQ.mjs.map +0 -7
- package/dist/lib/browser/chunk-GX4TUNM6.mjs.map +0 -7
- package/dist/lib/browser/chunk-JKWMHZP6.mjs.map +0 -7
- package/dist/lib/browser/chunk-LVJW5EFU.mjs +0 -157
- package/dist/lib/browser/chunk-LVJW5EFU.mjs.map +0 -7
- package/dist/lib/browser/chunk-RFSO3JRG.mjs +0 -1
- package/dist/lib/browser/chunk-WPE6AL7I.mjs +0 -905
- package/dist/lib/browser/chunk-WPE6AL7I.mjs.map +0 -7
- package/dist/lib/browser/invoker-capability-H5PPENOC.mjs.map +0 -7
- package/dist/lib/node-esm/capability-WFEG6CIZ.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs +0 -158
- package/dist/lib/node-esm/chunk-4A3ZCMI3.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-BCEOLX47.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-G3RTFSNG.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LQKOTNJW.mjs +0 -906
- package/dist/lib/node-esm/chunk-LQKOTNJW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-ULUEXB7Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-URWHJQT2.mjs.map +0 -7
- package/dist/lib/node-esm/invoker-capability-S3ZA527J.mjs.map +0 -7
- package/dist/types/src/vite-plugin/composer-plugin.d.ts +0 -18
- package/dist/types/src/vite-plugin/composer-plugin.d.ts.map +0 -1
- package/dist/types/src/vite-plugin/import-map-plugin.d.ts +0 -16
- package/dist/types/src/vite-plugin/import-map-plugin.d.ts.map +0 -1
- package/src/vite-plugin/composer-plugin.ts +0 -128
- package/src/vite-plugin/import-map-plugin.ts +0 -314
- /package/dist/lib/browser/{capability-BBBBAPDI.mjs.map → capability-Q5XRXRD2.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-I34GF4NG.mjs.map → chunk-45CHLTBV.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-RFSO3JRG.mjs.map → chunk-G7SDBRKH.mjs.map} +0 -0
- /package/dist/lib/node-esm/{capability-AWBEMRYR.mjs.map → capability-EW5GJCI6.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-WZCSOX5Q.mjs.map → chunk-6XW6LET6.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-EL3R25OQ.mjs.map → chunk-OZ7DZA5Z.mjs.map} +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ReactNode, useEffect, useLayoutEffect, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { ThemeProvider } from '@dxos/react-ui';
|
|
8
|
+
import { defaultTx, mx } from '@dxos/ui-theme';
|
|
9
|
+
|
|
10
|
+
import { type PlaceholderProps as PlaceholderSlotProps } from '../../hooks';
|
|
11
|
+
|
|
12
|
+
declare global {
|
|
13
|
+
interface Window {
|
|
14
|
+
/**
|
|
15
|
+
* Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`.
|
|
16
|
+
* Declared here so the {@link Placeholder} can dismiss the native-DOM
|
|
17
|
+
* loader once the React placeholder commits without each host having to
|
|
18
|
+
* re-declare the type.
|
|
19
|
+
*/
|
|
20
|
+
__bootLoader?: {
|
|
21
|
+
status: (payload: {
|
|
22
|
+
event?: string;
|
|
23
|
+
module?: string;
|
|
24
|
+
humanized: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional `(index/total)` tick. When present, the loader replaces the
|
|
27
|
+
* current line in place ("Loading plugins (12/80)") instead of
|
|
28
|
+
* appending a new entry — keeps the visible log compact during long
|
|
29
|
+
* counted phases like plugin chunk-loading or module activation.
|
|
30
|
+
*/
|
|
31
|
+
range?: { index: number; total: number };
|
|
32
|
+
}) => void;
|
|
33
|
+
progress: (fraction?: number) => void;
|
|
34
|
+
dismiss: () => void;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PlaceholderComponentProps = PlaceholderSlotProps & {
|
|
40
|
+
/**
|
|
41
|
+
* Brand mark rendered while the React tree settles. Pass either a static
|
|
42
|
+
* `ReactNode` (the framework wraps it in a sizing/animation div) or a
|
|
43
|
+
* render function that receives the precomputed `className` so the host
|
|
44
|
+
* can apply it directly to its logo (e.g. an SVG that accepts `className`
|
|
45
|
+
* — `composer-app` passes `(p) => <Composer {...p} />` from `@dxos/brand`).
|
|
46
|
+
*/
|
|
47
|
+
logo?: ReactNode | ((props: { className: string }) => ReactNode);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* React placeholder. The native-DOM boot loader is the visible UI through
|
|
52
|
+
* stages 0 (Loading) and 1 (FadeIn): we render `null` until `stage >= 2`
|
|
53
|
+
* (FadeOut) and instead feed activation progress + status into the still-
|
|
54
|
+
* alive boot loader. At stage 2 the loader dismisses and the React mark
|
|
55
|
+
* appears for the cross-fade to the real shell:
|
|
56
|
+
*
|
|
57
|
+
* - composer-app fills `0 → 50%` from `getPlugins`'s per-chunk counter.
|
|
58
|
+
* - this component fills `50 → 100%` from `useApp`'s
|
|
59
|
+
* `startupProgress.progress` (`activated / total` modules) and pushes
|
|
60
|
+
* the per-module status text to the loader's status line.
|
|
61
|
+
* - at `stage >= 2` the boot loader is dismissed and the React mark
|
|
62
|
+
* starts its `FadeOut → Done` shrink-and-fade.
|
|
63
|
+
*
|
|
64
|
+
* Stage progression (driven by `useApp`):
|
|
65
|
+
* - `0` — Loading: native-DOM boot loader visible. Placeholder renders nothing.
|
|
66
|
+
* - `1` — FadeIn: same — boot loader still owns the screen.
|
|
67
|
+
* - `2` — FadeOut: boot loader dismissed, React mark visible at full opacity then shrinks.
|
|
68
|
+
*/
|
|
69
|
+
export const Placeholder = ({ stage = 1, progress, logo }: PlaceholderComponentProps) => {
|
|
70
|
+
// Used in tests to exercise the error boundary & reset dialog.
|
|
71
|
+
if (location.search === '?throw') {
|
|
72
|
+
throw new Error('Test error');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Phase B: feed activation progress + status to the still-visible boot
|
|
76
|
+
// loader. Maps `[0, 1]` activation → `[0.5, 1]` of the ring; forwards
|
|
77
|
+
// the raw activation source (`event` / `module`) plus the pre-humanised
|
|
78
|
+
// label to `__bootLoader.status` so the loader gets to decide how to
|
|
79
|
+
// render and trace each transition (current default policy: "Activating
|
|
80
|
+
// <humanizedName>"; trace mode and timings track the structured fields).
|
|
81
|
+
// No-op once the loader has been dismissed (its `status()` early-returns
|
|
82
|
+
// when the boot DOM is gone). Skipping `stage >= 2` lets the dismissal
|
|
83
|
+
// run uncontended.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (stage >= 2) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const fraction = progress?.progress ?? 0;
|
|
89
|
+
window.__bootLoader?.progress(0.5 + fraction * 0.5);
|
|
90
|
+
if (progress?.humanizedName) {
|
|
91
|
+
window.__bootLoader?.status({
|
|
92
|
+
event: progress.event,
|
|
93
|
+
module: progress.module,
|
|
94
|
+
humanized: `Activating ${progress.humanizedName}`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}, [stage, progress?.progress, progress?.event, progress?.module, progress?.humanizedName]);
|
|
98
|
+
|
|
99
|
+
// Hand off from the native-DOM boot loader once the placeholder starts
|
|
100
|
+
// fading out — keeping the loader visible through `stage 0` and `stage 1`
|
|
101
|
+
// means the ring stays the visible source of truth for activation
|
|
102
|
+
// progress. `useLayoutEffect` runs before the next paint so the loader
|
|
103
|
+
// DOM is removed in the same frame the cross-fade begins.
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
if (stage >= 2) {
|
|
106
|
+
window.__bootLoader?.dismiss();
|
|
107
|
+
}
|
|
108
|
+
}, [stage]);
|
|
109
|
+
|
|
110
|
+
// Backstop for the fast-load path where `useLoading` can skip straight
|
|
111
|
+
// from stage 0 to `Done` (ready=true at the first debounce tick) — the
|
|
112
|
+
// stage-driven effect above never sees `stage >= 2`, so dismiss on
|
|
113
|
+
// unmount instead. The `stageRef` guard is what keeps StrictMode's
|
|
114
|
+
// synthetic mount → cleanup → remount cycle from prematurely dismissing
|
|
115
|
+
// the loader: the synthetic cleanup runs while `stageRef.current === 0`,
|
|
116
|
+
// skips the dismiss, and the real unmount (later, at stage `Done`)
|
|
117
|
+
// performs it.
|
|
118
|
+
const stageRef = useRef(stage);
|
|
119
|
+
stageRef.current = stage;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
return () => {
|
|
122
|
+
if (stageRef.current >= 3 /* Done */) {
|
|
123
|
+
window.__bootLoader?.dismiss();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
// Defer rendering anything until the FadeOut stage. The boot loader owns
|
|
129
|
+
// the screen during stages 0 and 1, so any DOM we render here would just
|
|
130
|
+
// sit invisibly behind it — and rendering null avoids paint cost during
|
|
131
|
+
// the longest part of the loading sequence (plugin activation).
|
|
132
|
+
if (stage < 2) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const logoClassName = mx(
|
|
137
|
+
'h-[300px] w-[300px] scale-600 transition-all duration-500 ease-in-out filter grayscale opacity-0',
|
|
138
|
+
stage >= 1 && 'scale-100 grayscale-0 opacity-70',
|
|
139
|
+
stage >= 2 && 'scale-50 opacity-0',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<ThemeProvider tx={defaultTx}>
|
|
144
|
+
<div role='none' className='relative dx-container h-dvh flex flex-col'>
|
|
145
|
+
<div role='none' className='flex flex-col grow justify-center items-center'>
|
|
146
|
+
{typeof logo === 'function' ? (
|
|
147
|
+
logo({ className: logoClassName })
|
|
148
|
+
) : logo ? (
|
|
149
|
+
<div className={logoClassName}>{logo}</div>
|
|
150
|
+
) : null}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</ThemeProvider>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
@@ -38,7 +38,9 @@ const CountStatus = () => {
|
|
|
38
38
|
}
|
|
39
39
|
}, [counter]);
|
|
40
40
|
|
|
41
|
-
if (!manager)
|
|
41
|
+
if (!manager) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
42
44
|
|
|
43
45
|
const isEven = count % 2 === 0;
|
|
44
46
|
|
|
@@ -170,7 +172,7 @@ const DefaultStory = () => {
|
|
|
170
172
|
};
|
|
171
173
|
|
|
172
174
|
const meta = {
|
|
173
|
-
title: 'sdk/app-framework/PluginManagerContext',
|
|
175
|
+
title: 'sdk/app-framework/components/PluginManagerContext',
|
|
174
176
|
render: DefaultStory,
|
|
175
177
|
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
176
178
|
parameters: {
|
|
@@ -129,7 +129,7 @@ const DefaultStory = () => {
|
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
const meta = {
|
|
132
|
-
title: 'sdk/app-framework/Surface',
|
|
132
|
+
title: 'sdk/app-framework/components/Surface',
|
|
133
133
|
render: DefaultStory,
|
|
134
134
|
decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withPluginManager({ capabilities: [] })],
|
|
135
135
|
parameters: {
|
|
@@ -27,7 +27,14 @@ import { usePluginManager } from '../PluginManager/PluginManagerProvider';
|
|
|
27
27
|
import { SurfaceContext } from './context';
|
|
28
28
|
import { SurfaceInfo } from './SurfaceInfo';
|
|
29
29
|
import { useSurfaceProfilerCallback } from './SurfaceProfilerContext';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
type Definition,
|
|
32
|
+
type Props,
|
|
33
|
+
type RoleToken,
|
|
34
|
+
type TokenData,
|
|
35
|
+
type TypedProps,
|
|
36
|
+
type WebComponentDefinition,
|
|
37
|
+
} from './types';
|
|
31
38
|
|
|
32
39
|
const DEBUG = import.meta.env.VITE_DEBUG;
|
|
33
40
|
|
|
@@ -38,7 +45,7 @@ const DEFAULT_PLACEHOLDER = <Fragment />;
|
|
|
38
45
|
* Handles creation, prop setting, and cleanup of Web Components.
|
|
39
46
|
*/
|
|
40
47
|
const WebComponentWrapper = memo(
|
|
41
|
-
forwardRef<HTMLElement, Props & { definition: WebComponentDefinition }>(
|
|
48
|
+
forwardRef<HTMLElement, Omit<Props, 'role'> & { role: string; definition: WebComponentDefinition }>(
|
|
42
49
|
({ id, role, data, limit, definition, ...rest }, forwardedRef) => {
|
|
43
50
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
44
51
|
const elementRef = useRef<HTMLElement | null>(null);
|
|
@@ -108,7 +115,7 @@ WebComponentWrapper.displayName = 'WebComponentWrapper';
|
|
|
108
115
|
*/
|
|
109
116
|
// TODO(burdon): Allow DebugPlugin to provide different fallback using react-ui ErrorFallback.
|
|
110
117
|
const SurfaceContextProvider = memo(
|
|
111
|
-
forwardRef<HTMLElement, Props & { definition: Definition }>(
|
|
118
|
+
forwardRef<HTMLElement, Omit<Props, 'role'> & { role: string; definition: Definition }>(
|
|
112
119
|
({ id, role, data, limit, fallback = ErrorFallback, definition, ...rest }, forwardedRef) => {
|
|
113
120
|
const contextValue = useMemo(() => ({ id, role, data }), [id, role, data]);
|
|
114
121
|
const onProfilerRender = useSurfaceProfilerCallback();
|
|
@@ -177,43 +184,53 @@ SurfaceContextProvider.displayName = 'SurfaceContextProvider';
|
|
|
177
184
|
* A surface is a named region of the screen that can be populated by plugins.
|
|
178
185
|
*/
|
|
179
186
|
// TODO(burdon): Remove `ref` since relying on this would be error prone.
|
|
180
|
-
export const SurfaceComponent
|
|
181
|
-
forwardRef
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
export const SurfaceComponent = memo(
|
|
188
|
+
forwardRef<HTMLElement, Props & { type?: RoleToken<any> }>(
|
|
189
|
+
({ id: _id, role, type, data: dataProp, limit, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
|
|
190
|
+
const data = useDefaultValue(dataProp, () => ({}));
|
|
191
|
+
// TODO(wittjosiah): This will make all surfaces depend on a single signal.
|
|
192
|
+
// This isn't ideal because it means that any change to the data will cause all surfaces to re-render.
|
|
193
|
+
// This effectively means that plugin modules which contribute surfaces need to all be activated at startup.
|
|
194
|
+
// This should be fine for now because it's how it worked prior to capabilities api anyway.
|
|
195
|
+
// In the future, it would be nice to be able to bucket the surface contributions by role.
|
|
196
|
+
const surfaces = useSurfaces();
|
|
197
|
+
|
|
198
|
+
const effectiveRole = role ?? type?.role;
|
|
199
|
+
if (effectiveRole == null) {
|
|
200
|
+
if (DEBUG) {
|
|
201
|
+
log.warn('Surface has neither `role` nor `type` prop', { id: _id });
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
198
205
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
206
|
+
// NOTE: Memoizing the candidates makes the surface not re-render based on reactivity within data.
|
|
207
|
+
const definitions = findCandidates(surfaces, { role: effectiveRole, data });
|
|
208
|
+
const candidates = limit ? definitions.slice(0, limit) : definitions;
|
|
209
|
+
if (DEBUG && candidates.length === 0) {
|
|
210
|
+
log.warn('no candidates for surface', { role: effectiveRole, data });
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<Suspense fallback={placeholder}>
|
|
216
|
+
{candidates.map((definition) => (
|
|
217
|
+
<SurfaceContextProvider
|
|
218
|
+
key={definition.id}
|
|
219
|
+
id={definition.id}
|
|
220
|
+
role={effectiveRole}
|
|
221
|
+
data={data}
|
|
222
|
+
limit={limit}
|
|
223
|
+
definition={definition}
|
|
224
|
+
ref={forwardedRef}
|
|
225
|
+
{...rest}
|
|
226
|
+
/>
|
|
227
|
+
))}
|
|
228
|
+
</Suspense>
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
),
|
|
232
|
+
) as (<TToken extends RoleToken<any>>(props: TypedProps<TToken> & RefAttributes<HTMLElement>) => React.ReactNode) &
|
|
233
|
+
NamedExoticComponent<Props & RefAttributes<HTMLElement>>;
|
|
217
234
|
|
|
218
235
|
SurfaceComponent.displayName = 'Surface';
|
|
219
236
|
|
|
@@ -227,12 +244,12 @@ const ErrorFallback = ({ error }: Props) => {
|
|
|
227
244
|
);
|
|
228
245
|
};
|
|
229
246
|
|
|
230
|
-
const findCandidates = (surfaces: Definition[], { role, data }:
|
|
247
|
+
const findCandidates = (surfaces: Definition[], { role, data }: { role: string; data: Props['data'] }) => {
|
|
231
248
|
return Object.values(surfaces)
|
|
232
249
|
.filter((definition) =>
|
|
233
250
|
Array.isArray(definition.role) ? definition.role.includes(role) : definition.role === role,
|
|
234
251
|
)
|
|
235
|
-
.filter(({ filter }) => (filter ? filter(data ?? {}) : true))
|
|
252
|
+
.filter(({ filter }) => (filter ? filter(data ?? {}, role) : true))
|
|
236
253
|
.toSorted(byPosition);
|
|
237
254
|
};
|
|
238
255
|
|
|
@@ -255,12 +272,32 @@ export const useSurfaces = () => {
|
|
|
255
272
|
|
|
256
273
|
/**
|
|
257
274
|
* @returns `true` if there is a contributed surface which matches the specified role & data, `false` otherwise.
|
|
275
|
+
*
|
|
276
|
+
* Two overloads:
|
|
277
|
+
* - Typed: pass a `type` role token and `data` is constrained to the token's
|
|
278
|
+
* declared contract (e.g. `AppSurface.Section` requires `attendableId`).
|
|
279
|
+
* - Legacy: pass a string `role` and `data` is untyped.
|
|
258
280
|
*/
|
|
259
|
-
export
|
|
281
|
+
export function isSurfaceAvailable<TToken extends RoleToken<any>>(
|
|
260
282
|
capabilityManager: CapabilityManager.CapabilityManager,
|
|
261
|
-
{
|
|
262
|
-
)
|
|
283
|
+
args: { type: TToken; data?: TokenData<TToken>; role?: never },
|
|
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 },
|
|
292
|
+
): boolean {
|
|
293
|
+
const effectiveRole = role ?? type?.role;
|
|
294
|
+
if (effectiveRole == null) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
263
297
|
const surfaces = capabilityManager.getAll(Capabilities.ReactSurface);
|
|
264
|
-
const candidates = findCandidates(surfaces.flat(), {
|
|
298
|
+
const candidates = findCandidates(surfaces.flat(), {
|
|
299
|
+
role: effectiveRole,
|
|
300
|
+
data: data as Props['data'],
|
|
301
|
+
});
|
|
265
302
|
return candidates.length > 0;
|
|
266
|
-
}
|
|
303
|
+
}
|
|
@@ -12,7 +12,18 @@ import {
|
|
|
12
12
|
useSurfaceProfilerEntries,
|
|
13
13
|
useSurfaceProfilerStats,
|
|
14
14
|
} from './SurfaceProfilerContext';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type Definition as SurfaceDefinition,
|
|
17
|
+
type RoleToken as SurfaceRoleToken,
|
|
18
|
+
type SurfaceBinding as SurfaceBindingType,
|
|
19
|
+
type SurfaceFilter as SurfaceFilterType,
|
|
20
|
+
type TokenData as SurfaceTokenData,
|
|
21
|
+
type TypedProps as SurfaceTypedProps,
|
|
22
|
+
create as createSurface,
|
|
23
|
+
createWeb as createWebSurface,
|
|
24
|
+
isSurfaceFilter as isSurfaceFilterFn,
|
|
25
|
+
makeType as makeTypeFn,
|
|
26
|
+
} from './types';
|
|
16
27
|
|
|
17
28
|
export namespace Surface {
|
|
18
29
|
export type Definition = SurfaceDefinition;
|
|
@@ -25,6 +36,14 @@ export namespace Surface {
|
|
|
25
36
|
export const Surface = SurfaceComponent;
|
|
26
37
|
export const isAvailable = isSurfaceAvailable;
|
|
27
38
|
|
|
39
|
+
export type RoleToken<TData> = SurfaceRoleToken<TData>;
|
|
40
|
+
export type Binding = SurfaceBindingType;
|
|
41
|
+
export type Filter<TData> = SurfaceFilterType<TData>;
|
|
42
|
+
export type TokenData<T> = SurfaceTokenData<T>;
|
|
43
|
+
export type TypedProps<TToken extends SurfaceRoleToken<any>> = SurfaceTypedProps<TToken>;
|
|
44
|
+
export const makeType = makeTypeFn;
|
|
45
|
+
export const isFilter = isSurfaceFilterFn;
|
|
46
|
+
|
|
28
47
|
export const ProfilerProvider = SurfaceProfilerProvider;
|
|
29
48
|
export const useProfilerCallback = useSurfaceProfilerCallback;
|
|
30
49
|
export const useProfilerEntries = useSurfaceProfilerEntries;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { type CapabilityManager } from '../../../core';
|
|
8
|
+
import { isSurfaceAvailable } from './SurfaceComponent';
|
|
9
|
+
import { type RoleToken, type SurfaceFilter, create, isSurfaceFilter, makeType } from './types';
|
|
10
|
+
|
|
11
|
+
describe('Surface.makeType', () => {
|
|
12
|
+
test('creates a role token carrying the role string', ({ expect }) => {
|
|
13
|
+
const token = makeType<{ subject: string }>('test-role');
|
|
14
|
+
expect(token.role).toBe('test-role');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('tokens with the same role are independent objects (identity-by-role)', ({ expect }) => {
|
|
18
|
+
const a = makeType<{ x: number }>('shared');
|
|
19
|
+
const b = makeType<{ x: number }>('shared');
|
|
20
|
+
expect(a).not.toBe(b);
|
|
21
|
+
expect(a.role).toBe(b.role);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('isSurfaceFilter', () => {
|
|
26
|
+
test('distinguishes filter objects from predicate functions', ({ expect }) => {
|
|
27
|
+
const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: 'r', guard: () => true }] };
|
|
28
|
+
expect(isSurfaceFilter(filter)).toBe(true);
|
|
29
|
+
expect(isSurfaceFilter(() => true)).toBe(false);
|
|
30
|
+
expect(isSurfaceFilter({})).toBe(false);
|
|
31
|
+
expect(isSurfaceFilter(null)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
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
|
+
test('expands a single-binding SurfaceFilter into a role string', ({ expect }) => {
|
|
50
|
+
const filter: SurfaceFilter<Record<string, any>> = {
|
|
51
|
+
bindings: [{ role: 'article', guard: (d) => (d as any).subject === 'ok' }],
|
|
52
|
+
};
|
|
53
|
+
const def = create({ id: 'typed-single', 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);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('expands a multi-binding SurfaceFilter into a role array with role-scoped guards', ({ expect }) => {
|
|
60
|
+
const filter: SurfaceFilter<Record<string, any>> = {
|
|
61
|
+
bindings: [
|
|
62
|
+
{ role: 'article', guard: (d) => (d as any).subject === 'a' },
|
|
63
|
+
{ role: 'section', guard: (d) => (d as any).subject === 's' },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const def = create({ id: 'typed-multi', filter, component: () => null });
|
|
67
|
+
expect(def.role).toEqual(['article', 'section']);
|
|
68
|
+
// Role-specific guard.
|
|
69
|
+
expect(def.filter!({ subject: 'a' }, 'article')).toBe(true);
|
|
70
|
+
expect(def.filter!({ subject: 'a' }, 'section')).toBe(false);
|
|
71
|
+
expect(def.filter!({ subject: 's' }, 'section')).toBe(true);
|
|
72
|
+
expect(def.filter!({ subject: 's' }, 'article')).toBe(false);
|
|
73
|
+
// Without role, any binding may match.
|
|
74
|
+
expect(def.filter!({ subject: 'a' })).toBe(true);
|
|
75
|
+
expect(def.filter!({ subject: 'unknown' })).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('passes position through untouched', ({ expect }) => {
|
|
79
|
+
const filter: SurfaceFilter<Record<string, any>> = { bindings: [{ role: 'r', guard: () => true }] };
|
|
80
|
+
const def = create({ id: 'pos', filter, component: () => null, position: 'fallback' });
|
|
81
|
+
expect(def.position).toBe('fallback');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('role token typing', () => {
|
|
86
|
+
test('makeType preserves TData through token use-sites', ({ expect }) => {
|
|
87
|
+
// Type-level smoke test: if TS compiles, we're fine. The `RoleToken<T>`
|
|
88
|
+
// phantom should not impose a runtime constraint.
|
|
89
|
+
const token: RoleToken<{ subject: number }> = makeType('numeric');
|
|
90
|
+
expect(token.role).toBe('numeric');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('isSurfaceAvailable typing', () => {
|
|
95
|
+
// These tests double as static assertions: the `@ts-expect-error` comments
|
|
96
|
+
// fail to compile if the surrounding expression typechecks, so they verify
|
|
97
|
+
// the typed overload narrows `data` to the token's declared contract.
|
|
98
|
+
const sectionToken = makeType<{ attendableId: string; subject: string }>('section');
|
|
99
|
+
const capabilityManager = { getAll: () => [] } as unknown as CapabilityManager.CapabilityManager;
|
|
100
|
+
|
|
101
|
+
test('typed overload accepts data matching the token contract', () => {
|
|
102
|
+
// No error — data has all required fields.
|
|
103
|
+
isSurfaceAvailable(capabilityManager, {
|
|
104
|
+
type: sectionToken,
|
|
105
|
+
data: { attendableId: 'id', subject: 'x' },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('typed overload rejects data missing required fields', () => {
|
|
110
|
+
// @ts-expect-error — `data` is missing `attendableId` required by the token.
|
|
111
|
+
isSurfaceAvailable(capabilityManager, { type: sectionToken, data: { subject: 'x' } });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('typed overload rejects data with wrong field type', () => {
|
|
115
|
+
// @ts-expect-error — `attendableId` must be a string, not a number.
|
|
116
|
+
isSurfaceAvailable(capabilityManager, { type: sectionToken, data: { attendableId: 123, subject: 'x' } });
|
|
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
|
+
});
|