@dxos/plugin-registry 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.
Files changed (127) hide show
  1. package/dist/lib/RegistryPlugin.mjs +15 -0
  2. package/dist/lib/RegistryPlugin.mjs.map +1 -0
  3. package/dist/lib/RegistryPlugin.node.mjs +770 -0
  4. package/dist/lib/RegistryPlugin.node.mjs.map +1 -0
  5. package/dist/lib/RegistryPlugin.workerd.mjs +8 -0
  6. package/dist/lib/RegistryPlugin.workerd.mjs.map +1 -0
  7. package/dist/lib/capabilities.mjs +11 -0
  8. package/dist/lib/capabilities.mjs.map +1 -0
  9. package/dist/lib/chunk-BaseRegistryArticle.mjs +103 -0
  10. package/dist/lib/chunk-BaseRegistryArticle.mjs.map +1 -0
  11. package/dist/lib/chunk-LoadPluginDialog.mjs +2 -0
  12. package/dist/lib/chunk-PluginArticle.mjs +87 -0
  13. package/dist/lib/chunk-PluginArticle.mjs.map +1 -0
  14. package/dist/lib/chunk-PublicRegistryArticle.mjs +152 -0
  15. package/dist/lib/chunk-PublicRegistryArticle.mjs.map +1 -0
  16. package/dist/lib/chunk-RegistryArticle.mjs +29 -0
  17. package/dist/lib/chunk-RegistryArticle.mjs.map +1 -0
  18. package/dist/lib/chunk-RegistrySettingsContainer.mjs +37 -0
  19. package/dist/lib/chunk-RegistrySettingsContainer.mjs.map +1 -0
  20. package/dist/lib/chunk-app-graph-builder.mjs +202 -0
  21. package/dist/lib/chunk-app-graph-builder.mjs.map +1 -0
  22. package/dist/lib/chunk-categories.mjs +23 -0
  23. package/dist/lib/chunk-categories.mjs.map +1 -0
  24. package/dist/lib/chunk-components.mjs +537 -0
  25. package/dist/lib/chunk-components.mjs.map +1 -0
  26. package/dist/lib/chunk-containers.mjs +80 -0
  27. package/dist/lib/chunk-containers.mjs.map +1 -0
  28. package/dist/lib/chunk-dev-plugin-loader.mjs +40 -0
  29. package/dist/lib/chunk-dev-plugin-loader.mjs.map +1 -0
  30. package/dist/lib/chunk-hooks.mjs +345 -0
  31. package/dist/lib/chunk-hooks.mjs.map +1 -0
  32. package/dist/lib/chunk-meta.mjs +40 -0
  33. package/dist/lib/chunk-meta.mjs.map +1 -0
  34. package/dist/lib/chunk-open-plugin-registry.mjs +12 -0
  35. package/dist/lib/chunk-open-plugin-registry.mjs.map +1 -0
  36. package/dist/lib/chunk-operation-handler.mjs +11 -0
  37. package/dist/lib/chunk-operation-handler.mjs.map +1 -0
  38. package/dist/lib/chunk-react-surface.mjs +109 -0
  39. package/dist/lib/chunk-react-surface.mjs.map +1 -0
  40. package/dist/lib/chunk-settings.mjs +24 -0
  41. package/dist/lib/chunk-settings.mjs.map +1 -0
  42. package/dist/lib/chunk-storage.mjs +36 -0
  43. package/dist/lib/chunk-storage.mjs.map +1 -0
  44. package/dist/lib/components.mjs +2 -0
  45. package/dist/lib/containers.mjs +2 -0
  46. package/dist/lib/index.mjs +3 -0
  47. package/dist/lib/meta.mjs +2 -0
  48. package/dist/lib/operations.mjs +7 -0
  49. package/dist/lib/operations.mjs.map +1 -0
  50. package/dist/lib/plugin.mjs +9 -0
  51. package/dist/lib/plugin.mjs.map +1 -0
  52. package/dist/lib/translations.mjs +66 -0
  53. package/dist/lib/translations.mjs.map +1 -0
  54. package/dist/lib/types.mjs +29 -0
  55. package/dist/lib/types.mjs.map +1 -0
  56. package/dist/types/src/capabilities/RegistryCategoryArticle.d.ts +11 -0
  57. package/dist/types/src/capabilities/RegistryCategoryArticle.d.ts.map +1 -0
  58. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -1
  59. package/dist/types/src/capabilities/dev-plugin-loader.d.ts +1 -1
  60. package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
  61. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  62. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  63. package/dist/types/src/capabilities/settings.d.ts +1 -1
  64. package/dist/types/src/categories.d.ts.map +1 -1
  65. package/dist/types/src/categories.test.d.ts +2 -0
  66. package/dist/types/src/categories.test.d.ts.map +1 -0
  67. package/dist/types/src/commands/registry/util.d.ts.map +1 -1
  68. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +1 -1
  69. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  70. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +1 -1
  71. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts +1 -1
  72. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts.map +1 -1
  73. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts +4 -1
  74. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -1
  75. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts +4 -1
  76. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  77. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts +1 -1
  78. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -1
  79. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +1 -1
  80. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -1
  81. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts +4 -7
  82. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts.map +1 -1
  83. package/dist/types/src/hooks/usePluginActions.d.ts.map +1 -1
  84. package/dist/types/src/meta.d.ts +0 -3
  85. package/dist/types/src/meta.d.ts.map +1 -1
  86. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -1
  87. package/dist/types/src/types.d.ts +2 -2
  88. package/dist/types/src/types.d.ts.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +38 -37
  91. package/src/capabilities/RegistryCategoryArticle.tsx +46 -0
  92. package/src/capabilities/app-graph-builder.ts +80 -74
  93. package/src/capabilities/react-surface.ts +85 -0
  94. package/src/categories.test.ts +45 -0
  95. package/src/categories.ts +10 -7
  96. package/src/commands/registry/util.ts +1 -1
  97. package/src/components/PluginList/PluginItem.tsx +5 -3
  98. package/src/components/PluginList/PluginList.stories.tsx +1 -1
  99. package/src/components/RegistrySettings/RegistrySettings.tsx +2 -2
  100. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +2 -2
  101. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +2 -0
  102. package/src/containers/PluginArticle/PluginArticle.tsx +5 -1
  103. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +2 -0
  104. package/src/hooks/usePluginActions.ts +8 -4
  105. package/src/meta.ts +5 -8
  106. package/src/operations/open-plugin-registry.ts +2 -2
  107. package/src/types.ts +2 -1
  108. package/dist/lib/neutral/RegistryPlugin.mjs +0 -27
  109. package/dist/lib/neutral/RegistryPlugin.mjs.map +0 -7
  110. package/dist/lib/neutral/RegistryPlugin.node.mjs +0 -844
  111. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +0 -7
  112. package/dist/lib/neutral/RegistryPlugin.workerd.mjs +0 -10
  113. package/dist/lib/neutral/RegistryPlugin.workerd.mjs.map +0 -7
  114. package/dist/lib/neutral/chunk-XK2JMH3H.mjs +0 -33
  115. package/dist/lib/neutral/chunk-XK2JMH3H.mjs.map +0 -7
  116. package/dist/lib/neutral/chunk-ZY37XLVF.mjs +0 -46
  117. package/dist/lib/neutral/chunk-ZY37XLVF.mjs.map +0 -7
  118. package/dist/lib/neutral/index.mjs +0 -25
  119. package/dist/lib/neutral/index.mjs.map +0 -7
  120. package/dist/lib/neutral/meta.json +0 -1
  121. package/dist/lib/neutral/open-plugin-registry-ID7LV3SF.mjs +0 -15
  122. package/dist/lib/neutral/open-plugin-registry-ID7LV3SF.mjs.map +0 -7
  123. package/dist/lib/neutral/plugin.mjs +0 -18
  124. package/dist/lib/neutral/plugin.mjs.map +0 -7
  125. package/dist/lib/neutral/translations.mjs +0 -72
  126. package/dist/lib/neutral/translations.mjs.map +0 -7
  127. package/src/capabilities/react-surface.tsx +0 -123
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-registry",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Braneframe plugin gallery",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -25,9 +25,9 @@
25
25
  "default": "./src/RegistryPlugin.tsx"
26
26
  },
27
27
  "types": "./dist/types/src/RegistryPlugin.d.ts",
28
- "workerd": "./dist/lib/neutral/RegistryPlugin.workerd.mjs",
29
- "node": "./dist/lib/neutral/RegistryPlugin.node.mjs",
30
- "default": "./dist/lib/neutral/RegistryPlugin.mjs"
28
+ "workerd": "./dist/lib/RegistryPlugin.workerd.mjs",
29
+ "node": "./dist/lib/RegistryPlugin.node.mjs",
30
+ "default": "./dist/lib/RegistryPlugin.mjs"
31
31
  },
32
32
  "#translations": "./src/translations.ts",
33
33
  "#types": "./src/types.ts"
@@ -36,17 +36,17 @@
36
36
  ".": {
37
37
  "source": "./src/index.ts",
38
38
  "types": "./dist/types/src/index.d.ts",
39
- "default": "./dist/lib/neutral/index.mjs"
39
+ "import": "./dist/lib/index.mjs"
40
40
  },
41
41
  "./plugin": {
42
42
  "source": "./src/plugin.ts",
43
43
  "types": "./dist/types/src/plugin.d.ts",
44
- "default": "./dist/lib/neutral/plugin.mjs"
44
+ "import": "./dist/lib/plugin.mjs"
45
45
  },
46
46
  "./translations": {
47
47
  "source": "./src/translations.ts",
48
48
  "types": "./dist/types/src/translations.d.ts",
49
- "default": "./dist/lib/neutral/translations.mjs"
49
+ "import": "./dist/lib/translations.mjs"
50
50
  }
51
51
  },
52
52
  "types": "dist/types/src/index.d.ts",
@@ -56,31 +56,32 @@
56
56
  "src"
57
57
  ],
58
58
  "dependencies": {
59
+ "@effect-atom/atom": "^0.5.3",
59
60
  "@effect-atom/atom-react": "^0.5.0",
60
61
  "@effect/cli": "0.75.2",
61
62
  "yaml": "^2.8.2",
62
- "@dxos/app-toolkit": "0.10.0",
63
- "@dxos/app-framework": "0.10.0",
64
- "@dxos/client": "0.10.0",
65
- "@dxos/client-protocol": "0.10.0",
66
- "@dxos/compute": "0.10.0",
67
- "@dxos/context": "0.10.0",
68
- "@dxos/echo": "0.10.0",
69
- "@dxos/edge-client": "0.10.0",
70
- "@dxos/invariant": "0.10.0",
71
- "@dxos/cli-util": "0.10.0",
72
- "@dxos/keys": "0.10.0",
73
- "@dxos/effect": "0.10.0",
74
- "@dxos/log": "0.10.0",
75
- "@dxos/plugin-graph": "0.10.0",
76
- "@dxos/protocols": "0.10.0",
77
- "@dxos/react-ui-list": "0.10.0",
78
- "@dxos/react-ui-markdown": "0.10.0",
79
- "@dxos/types": "0.10.0",
80
- "@dxos/react-ui-mosaic": "0.10.0",
81
- "@dxos/react-ui-form": "0.10.0",
82
- "@dxos/util": "0.10.0",
83
- "@dxos/plugin-observability": "0.10.0"
63
+ "@dxos/app-framework": "0.11.1",
64
+ "@dxos/app-toolkit": "0.11.1",
65
+ "@dxos/client": "0.11.1",
66
+ "@dxos/client-protocol": "0.11.1",
67
+ "@dxos/compute": "0.11.1",
68
+ "@dxos/cli-util": "0.11.1",
69
+ "@dxos/context": "0.11.1",
70
+ "@dxos/echo": "0.11.1",
71
+ "@dxos/edge-client": "0.11.1",
72
+ "@dxos/effect": "0.11.1",
73
+ "@dxos/invariant": "0.11.1",
74
+ "@dxos/keys": "0.11.1",
75
+ "@dxos/link": "0.11.1",
76
+ "@dxos/log": "0.11.1",
77
+ "@dxos/plugin-graph": "0.11.1",
78
+ "@dxos/plugin-observability": "0.11.1",
79
+ "@dxos/protocols": "0.11.1",
80
+ "@dxos/react-ui-form": "0.11.1",
81
+ "@dxos/react-ui-list": "0.11.1",
82
+ "@dxos/react-ui-markdown": "0.11.1",
83
+ "@dxos/react-ui-mosaic": "0.11.1",
84
+ "@dxos/util": "0.11.1"
84
85
  },
85
86
  "devDependencies": {
86
87
  "@effect-atom/atom-react": "^0.5.0",
@@ -90,12 +91,12 @@
90
91
  "effect": "3.21.4",
91
92
  "react": "~19.2.7",
92
93
  "react-dom": "~19.2.7",
93
- "vite": "^8.0.16",
94
- "@dxos/plugin-testing": "0.10.0",
95
- "@dxos/random": "0.10.0",
96
- "@dxos/react-ui": "0.10.0",
97
- "@dxos/storybook-utils": "0.10.0",
98
- "@dxos/ui-theme": "0.10.0"
94
+ "vite": "^8.1.4",
95
+ "@dxos/plugin-testing": "0.11.1",
96
+ "@dxos/random": "0.11.1",
97
+ "@dxos/react-ui": "0.11.1",
98
+ "@dxos/storybook-utils": "0.11.1",
99
+ "@dxos/ui-theme": "0.11.1"
99
100
  },
100
101
  "peerDependencies": {
101
102
  "@effect-atom/atom-react": "^0.5.0",
@@ -103,8 +104,8 @@
103
104
  "effect": "3.21.4",
104
105
  "react": "~19.2.7",
105
106
  "react-dom": "~19.2.7",
106
- "@dxos/react-ui": "0.10.0",
107
- "@dxos/ui-theme": "0.10.0"
107
+ "@dxos/react-ui": "^0.11.1",
108
+ "@dxos/ui-theme": "^0.11.1"
108
109
  },
109
110
  "publishConfig": {
110
111
  "access": "public"
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { usePluginManager } from '@dxos/app-framework/ui';
8
+
9
+ import { RegistryArticle } from '#containers';
10
+
11
+ import { type PluginPredicate, getCategoryPredicate } from '../categories';
12
+ import { useAutoTags, useRegistryPlugins, useRemotePluginIds } from '../hooks';
13
+
14
+ /**
15
+ * Resolves the {@link PluginPredicate} for a registry category against the live plugin list.
16
+ * Shared with the graph builder via {@link getCategoryPredicate} so the category lists and their counts agree.
17
+ */
18
+ const useCategoryPredicate = (category: string): PluginPredicate => {
19
+ const manager = usePluginManager();
20
+ const remoteIds = useRemotePluginIds();
21
+ const core = useMemo(() => manager.getCore(), [manager]);
22
+ const enabled = useMemo(() => manager.getEnabled(), [manager]);
23
+ return useMemo(
24
+ () => getCategoryPredicate(category, { core, enabled, remoteIds }),
25
+ [category, core, enabled, remoteIds],
26
+ );
27
+ };
28
+
29
+ export type RegistryCategoryArticleProps = {
30
+ category: string;
31
+ };
32
+
33
+ /**
34
+ * Renders the {@link RegistryArticle} surface for one registry category. Centralises the
35
+ * `usePluginManager` + `useRegistryPlugins` + `useAutoTags` wiring shared by every category surface,
36
+ * none of which can live in a surface's `props` mapper.
37
+ */
38
+ export const RegistryCategoryArticle = ({ category }: RegistryCategoryArticleProps) => {
39
+ const manager = usePluginManager();
40
+ const filter = useCategoryPredicate(category);
41
+ const { entries } = useRegistryPlugins();
42
+ const extraTagsById = useAutoTags(entries);
43
+ const filtered = useMemo(() => manager.getPlugins().filter(filter), [manager, filter]);
44
+
45
+ return <RegistryArticle id={category} plugins={filtered} extraTagsById={extraTagsById} />;
46
+ };
@@ -11,7 +11,7 @@ import { DXN } from '@dxos/keys';
11
11
  import { GraphBuilder, Node, NodeMatcher } from '@dxos/plugin-graph';
12
12
  import { Position } from '@dxos/util';
13
13
 
14
- import { REGISTRY_ID, REGISTRY_KEY, meta, registryCategoryId } from '#meta';
14
+ import { REGISTRY_ID, meta } from '#meta';
15
15
 
16
16
  import { getCategoryPredicate, getRemotePluginIds } from '../categories';
17
17
  import { LOAD_PLUGIN_DIALOG } from '../containers';
@@ -63,6 +63,27 @@ export default Capability.makeModule(
63
63
  GraphBuilder.createExtension({
64
64
  id: 'registry',
65
65
  match: NodeMatcher.whenRoot,
66
+ // REGISTRY_ID is a pinned workspace (the URL's workspace anchor), so it carries no key of its
67
+ // own; its category and plugin children are the addressable planks (see `categories`/`plugins`).
68
+ connector: () =>
69
+ Effect.succeed([
70
+ Node.make({
71
+ id: REGISTRY_ID,
72
+ type: meta.profile.key,
73
+ properties: {
74
+ label: ['plugin-registry.label', { ns: meta.profile.key }],
75
+ icon: 'ph--squares-four--regular',
76
+ disposition: 'pin-end',
77
+ position: Position.first,
78
+ testId: 'treeView.pluginRegistry',
79
+ },
80
+ }),
81
+ ]),
82
+ }),
83
+ GraphBuilder.createExtension({
84
+ id: 'categories',
85
+ url: { key: 'category', kind: 'item', path: [] },
86
+ match: NodeMatcher.whenId(`root/${REGISTRY_ID}`),
66
87
  connector: (_node, get) => {
67
88
  const manager = capabilities.get(Capabilities.PluginManager);
68
89
  const plugins = get(manager.plugins);
@@ -77,82 +98,64 @@ export default Capability.makeModule(
77
98
 
78
99
  return Effect.succeed([
79
100
  Node.make({
80
- id: REGISTRY_ID,
81
- type: meta.profile.key,
101
+ id: 'bundled',
102
+ type: 'category',
103
+ data: 'bundled',
82
104
  properties: {
83
- label: ['plugin-registry.label', { ns: meta.profile.key }],
105
+ label: ['bundled-plugins.label', { ns: meta.profile.key }],
84
106
  icon: 'ph--squares-four--regular',
85
- disposition: 'pin-end',
86
- position: Position.first,
87
- testId: 'treeView.pluginRegistry',
107
+ testId: 'pluginRegistry.bundled',
108
+ count: categoryCount('bundled'),
109
+ },
110
+ }),
111
+ Node.make({
112
+ id: 'installed',
113
+ type: 'category',
114
+ data: 'installed',
115
+ properties: {
116
+ label: ['installed-plugins.label', { ns: meta.profile.key }],
117
+ icon: 'ph--check--regular',
118
+ testId: 'pluginRegistry.installed',
119
+ count: categoryCount('installed'),
120
+ },
121
+ }),
122
+ Node.make({
123
+ id: 'recommended',
124
+ type: 'category',
125
+ data: 'recommended',
126
+ properties: {
127
+ label: ['recommended-plugins.label', { ns: meta.profile.key }],
128
+ icon: 'ph--star--regular',
129
+ testId: 'pluginRegistry.recommended',
130
+ count: categoryCount('recommended'),
131
+ },
132
+ }),
133
+ Node.make({
134
+ id: 'labs',
135
+ type: 'category',
136
+ data: 'labs',
137
+ properties: {
138
+ label: ['labs-plugins.label', { ns: meta.profile.key }],
139
+ icon: 'ph--flask--regular',
140
+ testId: 'pluginRegistry.labs',
141
+ count: categoryCount('labs'),
88
142
  },
89
- nodes: [
90
- Node.make({
91
- id: registryCategoryId('bundled'),
92
- type: 'category',
93
- data: registryCategoryId('bundled'),
94
- properties: {
95
- label: ['bundled-plugins.label', { ns: meta.profile.key }],
96
- icon: 'ph--squares-four--regular',
97
- key: REGISTRY_KEY,
98
- testId: 'pluginRegistry.bundled',
99
- count: categoryCount(registryCategoryId('bundled')),
100
- },
101
- }),
102
- Node.make({
103
- id: registryCategoryId('installed'),
104
- type: 'category',
105
- data: registryCategoryId('installed'),
106
- properties: {
107
- label: ['installed-plugins.label', { ns: meta.profile.key }],
108
- icon: 'ph--check--regular',
109
- key: REGISTRY_KEY,
110
- testId: 'pluginRegistry.installed',
111
- count: categoryCount(registryCategoryId('installed')),
112
- },
113
- }),
114
- Node.make({
115
- id: registryCategoryId('recommended'),
116
- type: 'category',
117
- data: registryCategoryId('recommended'),
118
- properties: {
119
- label: ['recommended-plugins.label', { ns: meta.profile.key }],
120
- icon: 'ph--star--regular',
121
- key: REGISTRY_KEY,
122
- testId: 'pluginRegistry.recommended',
123
- count: categoryCount(registryCategoryId('recommended')),
124
- },
125
- }),
126
- Node.make({
127
- id: registryCategoryId('labs'),
128
- type: 'category',
129
- data: registryCategoryId('labs'),
130
- properties: {
131
- label: ['labs-plugins.label', { ns: meta.profile.key }],
132
- icon: 'ph--flask--regular',
133
- key: REGISTRY_KEY,
134
- testId: 'pluginRegistry.labs',
135
- count: categoryCount(registryCategoryId('labs')),
136
- },
137
- }),
138
- ...(registryCount > 0
139
- ? [
140
- Node.make({
141
- id: registryCategoryId('registry'),
142
- type: 'category',
143
- data: registryCategoryId('registry'),
144
- properties: {
145
- label: ['registry-plugins.label', { ns: meta.profile.key }],
146
- icon: 'ph--users-three--regular',
147
- key: REGISTRY_KEY,
148
- testId: 'pluginRegistry.registry',
149
- count: registryCount,
150
- },
151
- }),
152
- ]
153
- : []),
154
- ],
155
143
  }),
144
+ ...(registryCount > 0
145
+ ? [
146
+ Node.make({
147
+ id: 'registry',
148
+ type: 'category',
149
+ data: 'registry',
150
+ properties: {
151
+ label: ['registry-plugins.label', { ns: meta.profile.key }],
152
+ icon: 'ph--users-three--regular',
153
+ testId: 'pluginRegistry.registry',
154
+ count: registryCount,
155
+ },
156
+ }),
157
+ ]
158
+ : []),
156
159
  ]);
157
160
  },
158
161
  }),
@@ -179,6 +182,7 @@ export default Capability.makeModule(
179
182
  }),
180
183
  GraphBuilder.createExtension({
181
184
  id: 'plugins',
185
+ url: { key: 'registry', kind: 'item', path: [] },
182
186
  match: NodeMatcher.whenId(`root/${REGISTRY_ID}`),
183
187
  connector: (_node, get) => {
184
188
  const manager = capabilities.get(Capabilities.PluginManager);
@@ -199,7 +203,9 @@ export default Capability.makeModule(
199
203
 
200
204
  const registryEntries = get(manager.pluginRegistry.plugins).entries;
201
205
  const registryNodes = registryEntries
202
- .filter((entry) => !installedIds.has(DXN.make(entry.profile.key)))
206
+ // `profile.key` is the bare NSID on both sides; comparing against a `dxn:`-prefixed
207
+ // URI here would never match and would duplicate every installed plugin's node.
208
+ .filter((entry) => !installedIds.has(entry.profile.key))
203
209
  .map((entry) => {
204
210
  const plugin = toDisplayPlugin(entry);
205
211
  return Node.make({
@@ -0,0 +1,85 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import { type ComponentProps } from 'react';
7
+
8
+ import { Capabilities, Capability, Plugin } from '@dxos/app-framework';
9
+ import { Surface } from '@dxos/app-framework/ui';
10
+ import { AppSurface } from '@dxos/app-toolkit/ui';
11
+
12
+ import { DisableDependentsAlert } from '#components';
13
+ import {
14
+ LOAD_PLUGIN_DIALOG,
15
+ LoadPluginDialog,
16
+ PluginArticle,
17
+ PublicRegistryArticle,
18
+ RegistrySettingsContainer,
19
+ } from '#containers';
20
+ import { DISABLE_DEPENDENTS_DIALOG, meta } from '#meta';
21
+
22
+ import { RegistryCategoryArticle } from './RegistryCategoryArticle';
23
+
24
+ export default Capability.makeModule(() =>
25
+ Effect.succeed(
26
+ Capability.contributes(Capabilities.ReactSurface, [
27
+ Surface.create({
28
+ id: 'bundled',
29
+ filter: AppSurface.literal(AppSurface.Article, 'bundled'),
30
+ component: RegistryCategoryArticle,
31
+ props: () => ({ category: 'bundled' }),
32
+ }),
33
+ Surface.create({
34
+ id: 'installed',
35
+ filter: AppSurface.literal(AppSurface.Article, 'installed'),
36
+ component: RegistryCategoryArticle,
37
+ props: () => ({ category: 'installed' }),
38
+ }),
39
+ Surface.create({
40
+ id: 'recommended',
41
+ filter: AppSurface.literal(AppSurface.Article, 'recommended'),
42
+ component: RegistryCategoryArticle,
43
+ props: () => ({ category: 'recommended' }),
44
+ }),
45
+ Surface.create({
46
+ id: 'labs',
47
+ filter: AppSurface.literal(AppSurface.Article, 'labs'),
48
+ component: RegistryCategoryArticle,
49
+ props: () => ({ category: 'labs' }),
50
+ }),
51
+ Surface.create({
52
+ id: 'registry',
53
+ filter: AppSurface.literal(AppSurface.Article, 'registry'),
54
+ component: PublicRegistryArticle,
55
+ props: () => ({ id: 'registry' }),
56
+ }),
57
+ Surface.create({
58
+ id: 'pluginDetails',
59
+ filter: AppSurface.subject(AppSurface.Article, Plugin.isPlugin),
60
+ component: PluginArticle,
61
+ props: ({ data: { subject } }) => ({ subject }),
62
+ }),
63
+ Surface.create({
64
+ id: LOAD_PLUGIN_DIALOG,
65
+ filter: AppSurface.component(AppSurface.Dialog, LOAD_PLUGIN_DIALOG),
66
+ component: LoadPluginDialog,
67
+ }),
68
+ Surface.create({
69
+ id: DISABLE_DEPENDENTS_DIALOG,
70
+ filter: AppSurface.component<ComponentProps<typeof DisableDependentsAlert>>(
71
+ AppSurface.Dialog,
72
+ DISABLE_DEPENDENTS_DIALOG,
73
+ ),
74
+ component: DisableDependentsAlert,
75
+ props: ({ data: { props } }) => ({ ...props }),
76
+ }),
77
+ Surface.create({
78
+ id: 'pluginSettings',
79
+ filter: AppSurface.settings(AppSurface.Article, meta.profile.key),
80
+ component: RegistrySettingsContainer,
81
+ props: ({ data: { subject } }) => ({ subject }),
82
+ }),
83
+ ]),
84
+ ),
85
+ );
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { Plugin } from '@dxos/app-framework';
8
+
9
+ import { getCategoryPredicate } from './categories';
10
+
11
+ const context = { core: [], enabled: [], remoteIds: new Set<string>() };
12
+
13
+ describe('getCategoryPredicate', () => {
14
+ test('recommended selects the beta and alpha tiers', ({ expect }) => {
15
+ const predicate = getCategoryPredicate('recommended', context);
16
+ expect(predicate(makePlugin('beta-plugin', ['beta']))).toBe(true);
17
+ expect(predicate(makePlugin('alpha-plugin', ['alpha']))).toBe(true);
18
+ expect(predicate(makePlugin('labs-plugin', ['labs']))).toBe(false);
19
+ // An untagged plugin makes no quality claim, so it must not be recommended.
20
+ expect(predicate(makePlugin('untagged-plugin'))).toBe(false);
21
+ });
22
+
23
+ test('recommended matches a tier tag in any position', ({ expect }) => {
24
+ const predicate = getCategoryPredicate('recommended', context);
25
+ expect(predicate(makePlugin('alpha-connector', ['alpha', 'connector']))).toBe(true);
26
+ expect(predicate(makePlugin('labs-connector', ['labs', 'connector']))).toBe(false);
27
+ });
28
+
29
+ test('recommended excludes core and remote plugins', ({ expect }) => {
30
+ const key = 'org.dxos.plugin.example';
31
+ const plugin = makePlugin(key, ['beta']);
32
+ expect(getCategoryPredicate('recommended', { ...context, core: [key] })(plugin)).toBe(false);
33
+ expect(getCategoryPredicate('recommended', { ...context, remoteIds: new Set([key]) })(plugin)).toBe(false);
34
+ });
35
+
36
+ test('labs selects the labs tier regardless of core membership', ({ expect }) => {
37
+ const key = 'labs-plugin';
38
+ const predicate = getCategoryPredicate('labs', { ...context, core: [key] });
39
+ expect(predicate(makePlugin(key, ['labs']))).toBe(true);
40
+ expect(predicate(makePlugin('alpha-plugin', ['alpha']))).toBe(false);
41
+ });
42
+ });
43
+
44
+ const makePlugin = (key: string, tags?: string[]) =>
45
+ Plugin.make(Plugin.define({ profile: { key, name: key, tags } }))();
package/src/categories.ts CHANGED
@@ -4,10 +4,11 @@
4
4
 
5
5
  import { type Plugin, UrlLoader } from '@dxos/app-framework';
6
6
 
7
- import { registryCategoryId } from './meta';
8
-
9
7
  export type PluginPredicate = (plugin: Plugin.Plugin) => boolean;
10
8
 
9
+ /** Quality tiers surfaced in `recommended`; an untagged plugin is excluded rather than assumed ready. */
10
+ const RECOMMENDED_TIERS: readonly string[] = ['beta', 'alpha'];
11
+
11
12
  export type CategoryFilterContext = {
12
13
  /** Core (bundled-by-default) plugin ids. */
13
14
  core: readonly string[];
@@ -26,14 +27,16 @@ export const getCategoryPredicate = (
26
27
  { core, enabled, remoteIds }: CategoryFilterContext,
27
28
  ): PluginPredicate => {
28
29
  switch (category) {
29
- case registryCategoryId('bundled'):
30
+ case 'bundled':
30
31
  return ({ meta }) => !core.includes(meta.profile.key) && !remoteIds.has(meta.profile.key);
31
- case registryCategoryId('installed'):
32
+ case 'installed':
32
33
  return ({ meta }) => !core.includes(meta.profile.key) && enabled.includes(meta.profile.key);
33
- case registryCategoryId('recommended'):
34
+ case 'recommended':
34
35
  return ({ meta }) =>
35
- !core.includes(meta.profile.key) && !remoteIds.has(meta.profile.key) && !meta.profile.tags?.includes('labs');
36
- case registryCategoryId('labs'):
36
+ !core.includes(meta.profile.key) &&
37
+ !remoteIds.has(meta.profile.key) &&
38
+ (meta.profile.tags?.some((tag) => RECOMMENDED_TIERS.includes(tag)) ?? false);
39
+ case 'labs':
37
40
  return ({ meta }) => meta.profile.tags?.includes('labs') ?? false;
38
41
  default:
39
42
  return () => false;
@@ -13,8 +13,8 @@ import * as Schema from 'effect/Schema';
13
13
  import { AppSpace } from '@dxos/app-toolkit';
14
14
  import { type Client } from '@dxos/client';
15
15
  import { Filter } from '@dxos/echo';
16
+ import { AccessToken } from '@dxos/link';
16
17
  import { ALL_NSIDS, NSID } from '@dxos/protocols';
17
- import { AccessToken } from '@dxos/types';
18
18
 
19
19
  export { ALL_NSIDS, NSID };
20
20
 
@@ -141,7 +141,7 @@ export const PluginItem = ({
141
141
  gridCols,
142
142
  // Override `Listbox.Item`'s default row chrome (flex/items-center/padding/cursor) so the
143
143
  // bespoke card grid stretches both columns to full height and controls its own padding.
144
- 'items-stretch p-0 pe-2 cursor-default h-[14rem] w-full gap-3 bg-modal-surface rounded-md overflow-hidden',
144
+ 'items-stretch p-0 pe-2 cursor-default h-[14rem] w-full gap-3 dx-modal-surface rounded-md overflow-hidden',
145
145
  )}
146
146
  >
147
147
  <div className={mx(gridRows, 'rounded-l-md', styles.surface)}>
@@ -218,8 +218,10 @@ export const PluginItem = ({
218
218
 
219
219
  const tagColors: Record<RegistryTagType, ChromaticPalette | NeutralPalette> = {
220
220
  new: 'rose',
221
- beta: 'teal',
222
- labs: 'blue',
221
+ // Tier hues ramp green -> blue -> purple so the ordering reads without knowing the labels.
222
+ beta: 'green',
223
+ alpha: 'blue',
224
+ labs: 'purple',
223
225
  popular: 'green',
224
226
  featured: 'pink',
225
227
  experimental: 'amber',
@@ -75,7 +75,7 @@ export default meta;
75
75
  type Story = StoryObj<typeof meta>;
76
76
 
77
77
  export const Default: Story = {
78
- decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'bg-deck-surface' })],
78
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'dx-deck-surface' })],
79
79
  parameters: {
80
80
  layout: 'fullscreen',
81
81
  },
@@ -96,7 +96,7 @@ export const RegistrySettings = ({
96
96
  <Form.Content>
97
97
  <Form.Section title={t('dev-plugin.section.title')}>
98
98
  <Message.Root valence='neutral'>
99
- <Message.Content>{t('dev-plugin.description')}</Message.Content>
99
+ <Message.Body>{t('dev-plugin.description')}</Message.Body>
100
100
  </Message.Root>
101
101
  <Form.Row label={t('dev-plugin.url.label')} description={t('dev-plugin.url.description')}>
102
102
  <Input.Root>
@@ -120,7 +120,7 @@ export const RegistrySettings = ({
120
120
  </Form.Row>
121
121
  {enabled && !loadedDevId && !busy && (
122
122
  <Message.Root valence='warning'>
123
- <Message.Content>{t('dev-plugin.not-loaded.message')}</Message.Content>
123
+ <Message.Body>{t('dev-plugin.not-loaded.message')}</Message.Body>
124
124
  </Message.Root>
125
125
  )}
126
126
  </Form.Section>
@@ -114,8 +114,8 @@ export const BaseRegistryArticle = composable<HTMLDivElement, BaseRegistryArticl
114
114
  (pluginId: string) =>
115
115
  invokePromise(LayoutOperation.Open, {
116
116
  subject: [getPluginPath(pluginId)],
117
- pivotId: id,
118
- positioning: 'end',
117
+ pivotId: getPluginPath(id),
118
+ disposition: 'add',
119
119
  }),
120
120
  [invokePromise, id],
121
121
  );
@@ -86,3 +86,5 @@ export const LoadPluginDialog = () => {
86
86
  </Dialog.Content>
87
87
  );
88
88
  };
89
+
90
+ LoadPluginDialog.displayName = 'LoadPluginDialog';
@@ -76,9 +76,11 @@ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
76
76
  (targetId: string) => {
77
77
  void invokePromise(LayoutOperation.Open, {
78
78
  subject: [getPluginPath(targetId)],
79
+ pivotId: getPluginPath(pluginId),
80
+ disposition: 'add',
79
81
  });
80
82
  },
81
- [invokePromise],
83
+ [invokePromise, pluginId],
82
84
  );
83
85
 
84
86
  const actions = usePluginActions({
@@ -117,3 +119,5 @@ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
117
119
  />
118
120
  );
119
121
  };
122
+
123
+ PluginArticle.displayName = 'PluginArticle';
@@ -57,3 +57,5 @@ export const RegistrySettingsContainer = ({ subject }: RegistrySettingsContainer
57
57
  />
58
58
  );
59
59
  };
60
+
61
+ RegistrySettingsContainer.displayName = 'RegistrySettingsContainer';