@dxos/app-framework 0.10.0 → 0.11.1
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 +120 -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,70 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Button, Input, Popover } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
export type NamePopoverProps = PropsWithChildren<{
|
|
10
|
+
open: boolean;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
/** Translated label for the submit button (e.g. "Create"). */
|
|
13
|
+
submitLabel: string;
|
|
14
|
+
onSubmit: (name: string) => void;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Name-entry popover anchored to its trigger (mirrors the object rename popover UX).
|
|
20
|
+
* Enter or the submit button commits — an empty name is allowed; Escape or dismissal cancels.
|
|
21
|
+
*/
|
|
22
|
+
export const NamePopover = ({ children, open, placeholder, submitLabel, onSubmit, onCancel }: NamePopoverProps) => {
|
|
23
|
+
const [value, setValue] = useState('');
|
|
24
|
+
|
|
25
|
+
const submit = () => {
|
|
26
|
+
onSubmit(value);
|
|
27
|
+
setValue('');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const cancel = () => {
|
|
31
|
+
onCancel();
|
|
32
|
+
setValue('');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Popover.Root open={open} onOpenChange={(next) => !next && cancel()}>
|
|
37
|
+
<Popover.Trigger asChild>{children}</Popover.Trigger>
|
|
38
|
+
<Popover.Portal>
|
|
39
|
+
<Popover.Content>
|
|
40
|
+
<div className='flex items-center gap-1 p-2'>
|
|
41
|
+
<Input.Root>
|
|
42
|
+
<Input.Label srOnly>{placeholder}</Input.Label>
|
|
43
|
+
<Input.TextInput
|
|
44
|
+
autoFocus
|
|
45
|
+
placeholder={placeholder}
|
|
46
|
+
value={value}
|
|
47
|
+
onChange={(event) => setValue(event.target.value)}
|
|
48
|
+
onKeyDown={(event) => {
|
|
49
|
+
if (event.key === 'Enter') {
|
|
50
|
+
submit();
|
|
51
|
+
} else if (event.key === 'Escape') {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
event.stopPropagation();
|
|
54
|
+
cancel();
|
|
55
|
+
}
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
</Input.Root>
|
|
59
|
+
<Button variant='primary' onClick={submit}>
|
|
60
|
+
{submitLabel}
|
|
61
|
+
</Button>
|
|
62
|
+
</div>
|
|
63
|
+
<Popover.Arrow />
|
|
64
|
+
</Popover.Content>
|
|
65
|
+
</Popover.Portal>
|
|
66
|
+
</Popover.Root>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
NamePopover.displayName = 'NamePopover';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Surface
|
|
2
|
+
|
|
3
|
+
A Surface is a named region of the UI that is populated dynamically by plugins.
|
|
4
|
+
The consumer declares _what kind_ of region it is (a role) and _what data_ it
|
|
5
|
+
carries; the framework resolves which plugin-contributed component(s) render there.
|
|
6
|
+
This decouples the shell/layout from the plugins that fill it.
|
|
7
|
+
|
|
8
|
+
## Concepts
|
|
9
|
+
|
|
10
|
+
- **RoleToken** (`makeType<TData>(nsid)`) — a typed dispatch key. Carries a role
|
|
11
|
+
NSID (e.g. `org.dxos.role.article`) plus a phantom `TData` so consumers
|
|
12
|
+
(`type={Token}`) and providers (`makeFilter(Token, guard)`) share one
|
|
13
|
+
type-level contract. Identity is structural: same `role` string ⇒ interchangeable.
|
|
14
|
+
- **SurfaceFilter** (`makeFilter(token, guard?)`) — one or more `(role, guard)`
|
|
15
|
+
bindings. The guard validates the data shape at dispatch time; omitting it
|
|
16
|
+
matches any data at the role.
|
|
17
|
+
- **Definition** — a registered contribution: `{ kind, id, role, position?, filter, … }`.
|
|
18
|
+
`kind: 'react'` carries a `component`; `kind: 'web-component'` carries a `tagName`.
|
|
19
|
+
Authored via `create` / `createWeb`; registered as the `Capabilities.ReactSurface`
|
|
20
|
+
capability.
|
|
21
|
+
|
|
22
|
+
## Dispatch pipeline
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
<Surface type={Token} data={…} limit?={n} />
|
|
26
|
+
└─ useSurfaces() → role-indexed, position-sorted candidate map
|
|
27
|
+
└─ findCandidates(role,data)→ candidates for role, then data guard filter
|
|
28
|
+
└─ Suspense
|
|
29
|
+
└─ SurfaceContextProvider (per candidate) ← ErrorBoundary + SurfaceContext
|
|
30
|
+
└─ React component | WebComponentWrapper
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Indexing.** A per-manager derived atom builds a `Map<role, Definition[]>`
|
|
34
|
+
once per contribution change with candidates already sorted by
|
|
35
|
+
`Position.compare`. Per-render work is therefore just the data-dependent guard
|
|
36
|
+
filter over one role's bucket — no full scan or re-sort on every render. The
|
|
37
|
+
data guard is intentionally _not_ memoized so a Surface re-dispatches when
|
|
38
|
+
reactive `data` changes. `indexByRole` itself is a pure group-and-sort; the
|
|
39
|
+
`SurfaceManager` instance filters out definitions with an invalid local id
|
|
40
|
+
before indexing (warning once per id via instance state, not a module global —
|
|
41
|
+
so distinct manager instances, e.g. across tests, don't share warning state).
|
|
42
|
+
- **Per-role subscription.** Each Surface subscribes to a derived atom for _its
|
|
43
|
+
role only_ (`getCandidatesAtom`). The atom wraps the role's bucket with
|
|
44
|
+
`Data.array`, so the atom registry compares results structurally (`Equal.equals`)
|
|
45
|
+
and a contribution to another role recomputes to an equal value and is dropped —
|
|
46
|
+
that role's subscribers never re-render. Re-renders therefore scale with the
|
|
47
|
+
_affected role's_ surface count, not the total mounted fleet.
|
|
48
|
+
|
|
49
|
+
Quantified (`SurfaceComponent.test.tsx`, 100 surfaces across 10 roles, one
|
|
50
|
+
contribution per role): the old global subscription re-renders the whole fleet
|
|
51
|
+
on every contribution (100/contribution); per-role re-renders only the affected
|
|
52
|
+
role (20/contribution — one role's surfaces ×2 store commit phases) → **5×
|
|
53
|
+
fewer re-renders**, and the ratio grows with the number of roles. A second case
|
|
54
|
+
confirms 50 unrelated contributions cause 0 re-renders of an unaffected role.
|
|
55
|
+
|
|
56
|
+
- **`limit`.** Caps how many resolved candidates render (e.g. `limit={1}` for
|
|
57
|
+
single-component regions).
|
|
58
|
+
- **`useIsSurfaceAvailable`.** Same matching logic, exposed as `Surface.useIsAvailable`.
|
|
59
|
+
The hook captures the surface manager via context and returns a plain, stable
|
|
60
|
+
function — safe to store and call later (e.g. inside another callback such as
|
|
61
|
+
a render-prop or event handler) since the returned function calls no hooks
|
|
62
|
+
itself — used to probe whether a region would render without mounting it.
|
|
63
|
+
|
|
64
|
+
## Boundaries
|
|
65
|
+
|
|
66
|
+
Each matched candidate is wrapped in:
|
|
67
|
+
|
|
68
|
+
- **ErrorBoundary** (`name='surface'`, `resetKeys={[data]}`) — a failing surface
|
|
69
|
+
is contained and shown via the `fallback` component; sibling surfaces survive.
|
|
70
|
+
- **SurfaceContext** — exposes `{ id, role, data }` to descendants via
|
|
71
|
+
`useSurface()`. This is the supported way to read surface identity; the shell
|
|
72
|
+
must not reach _into_ a surface.
|
|
73
|
+
|
|
74
|
+
A single `Suspense` (the consumer's `placeholder`) wraps all candidates.
|
|
75
|
+
|
|
76
|
+
### No `ref`
|
|
77
|
+
|
|
78
|
+
A Surface is a boundary, not a handle. It may resolve to zero, one, or many
|
|
79
|
+
components, so a forwarded `ref` has no well-defined target. The component does
|
|
80
|
+
not accept or forward a `ref`; consumers that need an element should own one
|
|
81
|
+
inside their contributed component.
|
|
82
|
+
|
|
83
|
+
### Production vs. debug DOM
|
|
84
|
+
|
|
85
|
+
In production a React candidate renders directly inside its context provider —
|
|
86
|
+
no wrapper element — so a Surface adds no layout box of its own. Debug builds
|
|
87
|
+
wrap each candidate in a `<dx-surface>` element (see below) that carries
|
|
88
|
+
`data-id` / `data-role` and provides a measurable boundary for overlays.
|
|
89
|
+
|
|
90
|
+
## Web Components
|
|
91
|
+
|
|
92
|
+
`createWeb({ tagName, … })` contributes a custom element instead of a React
|
|
93
|
+
component. `WebComponentWrapper` imperatively creates the element once, assigns
|
|
94
|
+
surface props as element properties, keeps them in sync, and removes the element
|
|
95
|
+
on unmount. The same props (`id`, `role`, `data`, `limit`, …) are passed as for
|
|
96
|
+
React surfaces.
|
|
97
|
+
|
|
98
|
+
## Debugging
|
|
99
|
+
|
|
100
|
+
Enabled when `VITE_DEBUG` is set (build-time) or the runtime flag is toggled
|
|
101
|
+
(no rebuild). When on:
|
|
102
|
+
|
|
103
|
+
- Each React candidate is wrapped in a `<dx-surface data-role data-id>` element —
|
|
104
|
+
named, inspectable in DevTools, and queryable
|
|
105
|
+
(`document.querySelectorAll('dx-surface')`).
|
|
106
|
+
- A single centralized overlay manager draws boundary highlights and an info
|
|
107
|
+
affordance, instead of every surface mounting its own portal and observers.
|
|
108
|
+
- The React `Profiler` is attached per surface; `SurfaceProfiler*` collect
|
|
109
|
+
render counts and durations for an in-app perf panel.
|
|
110
|
+
|
|
111
|
+
### Dev metrics
|
|
112
|
+
|
|
113
|
+
`SurfaceMetrics` (a module singleton, so the out-of-tree overlay and the in-app
|
|
114
|
+
devtools panel share one source) records, keyed by `surface/<id>/<role>`:
|
|
115
|
+
|
|
116
|
+
- **`dataUnstable` / `dataChurn`** — the consumer's `data` prop identity churns
|
|
117
|
+
across renders without its value changing. This is the most common Surface
|
|
118
|
+
footgun (see Performance notes) and nothing else detects it.
|
|
119
|
+
- **`candidates` / `truncated`** — candidates matched on the last dispatch, and
|
|
120
|
+
whether `limit` dropped some. Diagnoses "nothing renders" (0) and "two things
|
|
121
|
+
render" (ambiguous match).
|
|
122
|
+
- **`dispatches`** — how often the dispatcher re-resolved (the count the per-role
|
|
123
|
+
subscription minimizes).
|
|
124
|
+
- **`errors`** — error-boundary trips for this surface.
|
|
125
|
+
- **`mounts` / `unmounts`** — mount churn (often a thrashing upstream `key`).
|
|
126
|
+
|
|
127
|
+
Recording is gated on the debug flag (zero production cost). Surfaced two ways:
|
|
128
|
+
the `dx-surface` overlay badge (turns ⚠/red on a concern; full metrics on
|
|
129
|
+
expand) and the devtools `SurfaceProfilerPanel` (joined onto render-timing by id).
|
|
130
|
+
`Surface.useMetrics()` / `Surface.clearMetrics()` expose the data.
|
|
131
|
+
|
|
132
|
+
## Performance notes
|
|
133
|
+
|
|
134
|
+
- Keep `data` referentially stable across renders — it is a memo/`resetKeys`
|
|
135
|
+
dependency. Unstable `data` defeats candidate stability and trips error-boundary
|
|
136
|
+
resets.
|
|
@@ -3,21 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React, { useCallback, useEffect,
|
|
6
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { random } from '@dxos/random';
|
|
9
|
-
import { Panel, Toolbar } from '@dxos/react-ui';
|
|
9
|
+
import { Input, Panel, Toolbar } from '@dxos/react-ui';
|
|
10
10
|
import { Listbox } from '@dxos/react-ui-list';
|
|
11
11
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
12
|
import { type ColorStyles, getHashStyles, mx } from '@dxos/ui-theme';
|
|
13
13
|
|
|
14
14
|
import { Capabilities } from '../../../common';
|
|
15
|
+
import * as Role from '../../../common/Role';
|
|
15
16
|
import { withPluginManager } from '../../../testing';
|
|
16
17
|
import { usePluginManager } from '../PluginManager';
|
|
17
18
|
import { SurfaceComponent, useSurfaces } from './SurfaceComponent';
|
|
18
|
-
import {
|
|
19
|
+
import { isSurfaceDebugEnabled, setSurfaceDebug } from './SurfaceDebug';
|
|
20
|
+
import { create, makeFilter } from './types';
|
|
19
21
|
|
|
20
|
-
const ItemRole =
|
|
22
|
+
const ItemRole = Role.make<{ id: string }>('org.dxos.test.role.item');
|
|
21
23
|
|
|
22
24
|
type TestComponentProps = {
|
|
23
25
|
id: string;
|
|
@@ -26,19 +28,61 @@ type TestComponentProps = {
|
|
|
26
28
|
|
|
27
29
|
const TestComponent = ({ styles, id }: TestComponentProps) => {
|
|
28
30
|
return (
|
|
29
|
-
<div className={mx('flex justify-center items-center border rounded-sm', styles.
|
|
30
|
-
<span className={mx('dx-tag font-mono text-lg', styles.
|
|
31
|
+
<div className={mx('flex justify-center items-center border rounded-sm', styles.bg, styles.border)}>
|
|
32
|
+
<span className={mx('dx-tag font-mono text-lg', styles.fg)}>{id}</span>
|
|
31
33
|
</div>
|
|
32
34
|
);
|
|
33
35
|
};
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const ErrorComponent = () => {
|
|
38
|
+
const [count, setCount] = useState(3);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const interval = setInterval(() => {
|
|
41
|
+
setCount((count) => {
|
|
42
|
+
if (count <= 1) {
|
|
43
|
+
clearInterval(interval);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return count - 1;
|
|
47
|
+
});
|
|
48
|
+
}, 1_000);
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
if (count <= 0) {
|
|
53
|
+
throw new Error('BANG!');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className='flex justify-center items-center border border-rose-bg rounded-sm'>
|
|
58
|
+
<span className='font-mono'>Ticking... {count}</span>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type StoryProps = {
|
|
64
|
+
debug?: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const DefaultStory = ({ debug: debugProp }: StoryProps) => {
|
|
36
68
|
const manager = usePluginManager();
|
|
37
69
|
const surfaces = useSurfaces();
|
|
38
70
|
const [selected, setSelected] = useState<string | undefined>();
|
|
71
|
+
const [debug, setDebug] = useState(debugProp ?? isSurfaceDebugEnabled());
|
|
72
|
+
|
|
73
|
+
// Restore the global debug flag on unmount so other stories don't inherit it.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const previous = debugProp ?? isSurfaceDebugEnabled();
|
|
76
|
+
return () => setSurfaceDebug(previous);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
const handleToggleDebug = useCallback((next: boolean) => {
|
|
80
|
+
setSurfaceDebug(next);
|
|
81
|
+
setDebug(next);
|
|
82
|
+
}, []);
|
|
39
83
|
|
|
40
84
|
const handleAdd = useCallback(() => {
|
|
41
|
-
const id = `test
|
|
85
|
+
const id = `test${random.number.int({ min: 0, max: 1_000 })}`;
|
|
42
86
|
const styles = getHashStyles(id);
|
|
43
87
|
|
|
44
88
|
manager.capabilities.contribute({
|
|
@@ -65,42 +109,13 @@ const DefaultStory = () => {
|
|
|
65
109
|
implementation: create({
|
|
66
110
|
id: 'error',
|
|
67
111
|
filter: makeFilter(ItemRole, (data) => data.id === 'error'),
|
|
68
|
-
component:
|
|
69
|
-
const [count, setCount] = useState(3);
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
const interval = setInterval(() => {
|
|
72
|
-
setCount((count) => {
|
|
73
|
-
if (count <= 1) {
|
|
74
|
-
clearInterval(interval);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return count - 1;
|
|
78
|
-
});
|
|
79
|
-
}, 1_000);
|
|
80
|
-
return () => clearInterval(interval);
|
|
81
|
-
}, []);
|
|
82
|
-
|
|
83
|
-
if (count <= 0) {
|
|
84
|
-
throw new Error('BANG!');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className='flex justify-center items-center border border-rose-bg rounded-sm'>
|
|
89
|
-
<span className='font-mono'>Ticking... {count}</span>
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
},
|
|
112
|
+
component: ErrorComponent,
|
|
93
113
|
}),
|
|
94
114
|
});
|
|
95
115
|
|
|
96
116
|
setSelected('error');
|
|
97
117
|
}, [manager]);
|
|
98
118
|
|
|
99
|
-
const ref = useRef<HTMLElement>(null);
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
console.log(ref.current);
|
|
102
|
-
}, [ref]);
|
|
103
|
-
|
|
104
119
|
return (
|
|
105
120
|
<Panel.Root>
|
|
106
121
|
<Panel.Toolbar asChild>
|
|
@@ -108,10 +123,20 @@ const DefaultStory = () => {
|
|
|
108
123
|
<Toolbar.Button onClick={handleAdd}>Add</Toolbar.Button>
|
|
109
124
|
<Toolbar.Button onClick={handleSelect}>Pick</Toolbar.Button>
|
|
110
125
|
<Toolbar.Button onClick={handleError}>Error</Toolbar.Button>
|
|
126
|
+
<Toolbar.Separator />
|
|
127
|
+
<Input.Root>
|
|
128
|
+
<Input.Label classNames='pr-1'>Debug</Input.Label>
|
|
129
|
+
<Input.Switch checked={debug} onCheckedChange={handleToggleDebug} />
|
|
130
|
+
</Input.Root>
|
|
111
131
|
</Toolbar.Root>
|
|
112
132
|
</Panel.Toolbar>
|
|
113
|
-
<Panel.Content
|
|
114
|
-
<SurfaceComponent
|
|
133
|
+
<Panel.Content classNames='grid grid-cols-2 h-full gap-4 overflow-hidden'>
|
|
134
|
+
<SurfaceComponent
|
|
135
|
+
key={debug ? 'debug' : 'prod'}
|
|
136
|
+
type={ItemRole}
|
|
137
|
+
data={selected ? { id: selected } : undefined}
|
|
138
|
+
limit={1}
|
|
139
|
+
/>
|
|
115
140
|
<div className='overflow-y-auto h-full'>
|
|
116
141
|
<Listbox.Root>
|
|
117
142
|
<Listbox.Content aria-label='Surfaces'>
|
|
@@ -141,4 +166,8 @@ export default meta;
|
|
|
141
166
|
|
|
142
167
|
type Story = StoryObj<typeof meta>;
|
|
143
168
|
|
|
144
|
-
export const Default: Story = {
|
|
169
|
+
export const Default: Story = {
|
|
170
|
+
args: {
|
|
171
|
+
debug: true,
|
|
172
|
+
},
|
|
173
|
+
};
|