@dxos/plugin-registry 0.8.4-main.d05673bc65 → 0.8.4-main.dfabb4ec29

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 (234) hide show
  1. package/dist/lib/neutral/RegistryPlugin.mjs +27 -0
  2. package/dist/lib/neutral/RegistryPlugin.mjs.map +7 -0
  3. package/dist/lib/{browser/cli/index.mjs → neutral/RegistryPlugin.node.mjs} +28 -97
  4. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +7 -0
  5. package/dist/lib/neutral/chunk-XK2JMH3H.mjs +33 -0
  6. package/dist/lib/neutral/chunk-XK2JMH3H.mjs.map +7 -0
  7. package/dist/lib/{browser/chunk-KMUDYITM.mjs → neutral/chunk-YV7A523U.mjs} +11 -3
  8. package/dist/lib/neutral/chunk-YV7A523U.mjs.map +7 -0
  9. package/dist/lib/neutral/index.mjs +21 -0
  10. package/dist/lib/neutral/index.mjs.map +7 -0
  11. package/dist/lib/neutral/meta.json +1 -0
  12. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs +15 -0
  13. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs.map +7 -0
  14. package/dist/lib/neutral/plugin.mjs +18 -0
  15. package/dist/lib/neutral/plugin.mjs.map +7 -0
  16. package/dist/lib/neutral/translations.mjs +61 -0
  17. package/dist/lib/neutral/translations.mjs.map +7 -0
  18. package/dist/types/src/RegistryPlugin.d.ts +1 -0
  19. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  20. package/dist/types/src/{cli/plugin.d.ts → RegistryPlugin.node.d.ts} +2 -1
  21. package/dist/types/src/RegistryPlugin.node.d.ts.map +1 -0
  22. package/dist/types/src/RegistryPlugin.test.d.ts +2 -0
  23. package/dist/types/src/RegistryPlugin.test.d.ts.map +1 -0
  24. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  25. package/dist/types/src/capabilities/dev-plugin-loader.d.ts +11 -0
  26. package/dist/types/src/capabilities/dev-plugin-loader.d.ts.map +1 -0
  27. package/dist/types/src/capabilities/index.d.ts +15 -3
  28. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  29. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  30. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  31. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  32. package/dist/types/src/capabilities/settings.d.ts +14 -0
  33. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  34. package/dist/types/src/{cli/commands → commands}/disable.d.ts +2 -2
  35. package/dist/types/src/commands/disable.d.ts.map +1 -0
  36. package/dist/types/src/{cli/commands → commands}/enable.d.ts +2 -2
  37. package/dist/types/src/commands/enable.d.ts.map +1 -0
  38. package/dist/types/src/commands/index.d.ts.map +1 -0
  39. package/dist/types/src/commands/list.d.ts +7 -0
  40. package/dist/types/src/commands/list.d.ts.map +1 -0
  41. package/dist/types/src/commands/util.d.ts.map +1 -0
  42. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +39 -2
  43. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  44. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +51 -16
  45. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  46. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +20 -0
  47. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -0
  48. package/dist/types/src/components/PluginFailureBadge/index.d.ts +2 -0
  49. package/dist/types/src/components/PluginFailureBadge/index.d.ts.map +1 -0
  50. package/dist/types/src/components/PluginList/PluginItem.d.ts +29 -2
  51. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  52. package/dist/types/src/components/PluginList/PluginList.d.ts +18 -3
  53. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
  54. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +49 -16
  55. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +18 -0
  57. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -0
  58. package/dist/types/src/components/RegistrySettings/index.d.ts +2 -0
  59. package/dist/types/src/components/RegistrySettings/index.d.ts.map +1 -0
  60. package/dist/types/src/components/index.d.ts +2 -0
  61. package/dist/types/src/components/index.d.ts.map +1 -1
  62. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts +20 -0
  63. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts.map +1 -0
  64. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts +2 -0
  65. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts.map +1 -0
  66. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts +4 -0
  67. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -0
  68. package/dist/types/src/containers/LoadPluginDialog/index.d.ts +2 -0
  69. package/dist/types/src/containers/LoadPluginDialog/index.d.ts.map +1 -0
  70. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  71. package/dist/types/src/containers/PluginArticle/index.d.ts +1 -2
  72. package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -1
  73. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts +8 -0
  74. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -0
  75. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +2 -0
  76. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -0
  77. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +14 -0
  78. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -0
  79. package/dist/types/src/containers/RegistryArticle/index.d.ts +2 -0
  80. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -0
  81. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts +13 -0
  82. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts.map +1 -0
  83. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts +2 -0
  84. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts.map +1 -0
  85. package/dist/types/src/containers/index.d.ts +5 -1
  86. package/dist/types/src/containers/index.d.ts.map +1 -1
  87. package/dist/types/src/hooks/index.d.ts +4 -0
  88. package/dist/types/src/hooks/index.d.ts.map +1 -0
  89. package/dist/types/src/hooks/use-auto-tags.d.ts +20 -0
  90. package/dist/types/src/hooks/use-auto-tags.d.ts.map +1 -0
  91. package/dist/types/src/hooks/use-registry-plugins.d.ts +15 -0
  92. package/dist/types/src/hooks/use-registry-plugins.d.ts.map +1 -0
  93. package/dist/types/src/hooks/use-update-available.d.ts +18 -0
  94. package/dist/types/src/hooks/use-update-available.d.ts.map +1 -0
  95. package/dist/types/src/index.d.ts +1 -1
  96. package/dist/types/src/index.d.ts.map +1 -1
  97. package/dist/types/src/meta.d.ts +3 -1
  98. package/dist/types/src/meta.d.ts.map +1 -1
  99. package/dist/types/src/operations/index.d.ts +3 -0
  100. package/dist/types/src/operations/index.d.ts.map +1 -0
  101. package/dist/types/src/operations/open-plugin-registry.d.ts +5 -0
  102. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -0
  103. package/dist/types/src/plugin.d.ts +4 -0
  104. package/dist/types/src/plugin.d.ts.map +1 -0
  105. package/dist/types/src/storage.d.ts.map +1 -0
  106. package/dist/types/src/translations.d.ts +48 -16
  107. package/dist/types/src/translations.d.ts.map +1 -1
  108. package/dist/types/src/types.d.ts +30 -1
  109. package/dist/types/src/types.d.ts.map +1 -1
  110. package/dist/types/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +57 -36
  112. package/src/{cli/plugin.ts → RegistryPlugin.node.ts} +3 -1
  113. package/src/RegistryPlugin.test.ts +24 -0
  114. package/src/RegistryPlugin.tsx +13 -5
  115. package/src/capabilities/app-graph-builder.ts +200 -0
  116. package/src/capabilities/dev-plugin-loader.ts +39 -0
  117. package/src/capabilities/index.ts +11 -3
  118. package/src/capabilities/operation-handler.ts +16 -0
  119. package/src/capabilities/react-surface.tsx +136 -0
  120. package/src/capabilities/settings.ts +34 -0
  121. package/src/components/PluginDetail/PluginDetail.stories.tsx +4 -4
  122. package/src/components/PluginDetail/PluginDetail.tsx +175 -60
  123. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +68 -0
  124. package/src/components/PluginFailureBadge/index.ts +5 -0
  125. package/src/components/PluginList/PluginItem.tsx +116 -14
  126. package/src/components/PluginList/PluginList.stories.tsx +17 -14
  127. package/src/components/PluginList/PluginList.tsx +32 -4
  128. package/src/components/RegistrySettings/RegistrySettings.tsx +120 -0
  129. package/src/components/RegistrySettings/index.ts +5 -0
  130. package/src/components/index.ts +2 -0
  131. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +163 -0
  132. package/src/containers/BaseRegistryArticle/index.ts +5 -0
  133. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +87 -0
  134. package/src/containers/LoadPluginDialog/index.ts +5 -0
  135. package/src/containers/PluginArticle/PluginArticle.tsx +317 -9
  136. package/src/containers/PluginArticle/index.ts +1 -3
  137. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +192 -0
  138. package/src/containers/PublicRegistryArticle/index.ts +5 -0
  139. package/src/containers/RegistryArticle/RegistryArticle.tsx +53 -0
  140. package/src/containers/RegistryArticle/index.ts +5 -0
  141. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +59 -0
  142. package/src/containers/RegistrySettingsContainer/index.ts +5 -0
  143. package/src/containers/index.ts +5 -1
  144. package/src/hooks/index.ts +7 -0
  145. package/src/hooks/use-auto-tags.ts +51 -0
  146. package/src/hooks/use-registry-plugins.ts +29 -0
  147. package/src/hooks/use-update-available.ts +31 -0
  148. package/src/index.ts +1 -1
  149. package/src/meta.ts +10 -1
  150. package/src/operations/index.ts +7 -0
  151. package/src/operations/open-plugin-registry.ts +21 -0
  152. package/src/plugin.ts +11 -0
  153. package/src/translations.ts +52 -15
  154. package/src/types.ts +33 -1
  155. package/dist/lib/browser/PluginArticle-74DKRXJH.mjs +0 -29
  156. package/dist/lib/browser/PluginArticle-74DKRXJH.mjs.map +0 -7
  157. package/dist/lib/browser/PluginRegistry-DORQQ7GM.mjs +0 -78
  158. package/dist/lib/browser/PluginRegistry-DORQQ7GM.mjs.map +0 -7
  159. package/dist/lib/browser/app-graph-builder-AHS5BI4Y.mjs +0 -167
  160. package/dist/lib/browser/app-graph-builder-AHS5BI4Y.mjs.map +0 -7
  161. package/dist/lib/browser/chunk-KMUDYITM.mjs.map +0 -7
  162. package/dist/lib/browser/chunk-QQUG6FNW.mjs +0 -191
  163. package/dist/lib/browser/chunk-QQUG6FNW.mjs.map +0 -7
  164. package/dist/lib/browser/cli/index.mjs.map +0 -7
  165. package/dist/lib/browser/index.mjs +0 -65
  166. package/dist/lib/browser/index.mjs.map +0 -7
  167. package/dist/lib/browser/meta.json +0 -1
  168. package/dist/lib/browser/operation-resolver-PK4B2BZQ.mjs +0 -25
  169. package/dist/lib/browser/operation-resolver-PK4B2BZQ.mjs.map +0 -7
  170. package/dist/lib/browser/react-surface-HPLHM3SK.mjs +0 -85
  171. package/dist/lib/browser/react-surface-HPLHM3SK.mjs.map +0 -7
  172. package/dist/lib/node-esm/PluginArticle-JPVC7NXQ.mjs +0 -30
  173. package/dist/lib/node-esm/PluginArticle-JPVC7NXQ.mjs.map +0 -7
  174. package/dist/lib/node-esm/PluginRegistry-3ZB4DFF7.mjs +0 -79
  175. package/dist/lib/node-esm/PluginRegistry-3ZB4DFF7.mjs.map +0 -7
  176. package/dist/lib/node-esm/app-graph-builder-DBDL5WWF.mjs +0 -168
  177. package/dist/lib/node-esm/app-graph-builder-DBDL5WWF.mjs.map +0 -7
  178. package/dist/lib/node-esm/chunk-ENSNUFQK.mjs +0 -23
  179. package/dist/lib/node-esm/chunk-ENSNUFQK.mjs.map +0 -7
  180. package/dist/lib/node-esm/chunk-II5WBDYI.mjs +0 -192
  181. package/dist/lib/node-esm/chunk-II5WBDYI.mjs.map +0 -7
  182. package/dist/lib/node-esm/cli/index.mjs +0 -223
  183. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  184. package/dist/lib/node-esm/index.mjs +0 -66
  185. package/dist/lib/node-esm/index.mjs.map +0 -7
  186. package/dist/lib/node-esm/meta.json +0 -1
  187. package/dist/lib/node-esm/operation-resolver-CCHN4NBP.mjs +0 -26
  188. package/dist/lib/node-esm/operation-resolver-CCHN4NBP.mjs.map +0 -7
  189. package/dist/lib/node-esm/react-surface-GM64KKI3.mjs +0 -86
  190. package/dist/lib/node-esm/react-surface-GM64KKI3.mjs.map +0 -7
  191. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +0 -1
  192. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +0 -3
  193. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +0 -1
  194. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  195. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  196. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  197. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  198. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  199. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  200. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  201. package/dist/types/src/cli/commands/disable.d.ts.map +0 -1
  202. package/dist/types/src/cli/commands/enable.d.ts.map +0 -1
  203. package/dist/types/src/cli/commands/index.d.ts.map +0 -1
  204. package/dist/types/src/cli/commands/list.d.ts +0 -7
  205. package/dist/types/src/cli/commands/list.d.ts.map +0 -1
  206. package/dist/types/src/cli/commands/util.d.ts.map +0 -1
  207. package/dist/types/src/cli/index.d.ts +0 -3
  208. package/dist/types/src/cli/index.d.ts.map +0 -1
  209. package/dist/types/src/cli/plugin.d.ts.map +0 -1
  210. package/dist/types/src/cli/storage.d.ts.map +0 -1
  211. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +0 -8
  212. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +0 -1
  213. package/dist/types/src/containers/PluginRegistry/index.d.ts +0 -3
  214. package/dist/types/src/containers/PluginRegistry/index.d.ts.map +0 -1
  215. package/src/capabilities/app-graph-builder/app-graph-builder.ts +0 -137
  216. package/src/capabilities/app-graph-builder/index.ts +0 -7
  217. package/src/capabilities/operation-resolver/index.ts +0 -7
  218. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -24
  219. package/src/capabilities/react-surface/index.ts +0 -7
  220. package/src/capabilities/react-surface/react-surface.tsx +0 -88
  221. package/src/cli/index.ts +0 -6
  222. package/src/containers/PluginRegistry/PluginRegistry.tsx +0 -86
  223. package/src/containers/PluginRegistry/index.ts +0 -7
  224. /package/dist/types/src/capabilities/{app-graph-builder/app-graph-builder.d.ts → app-graph-builder.d.ts} +0 -0
  225. /package/dist/types/src/capabilities/{react-surface/react-surface.d.ts → react-surface.d.ts} +0 -0
  226. /package/dist/types/src/{cli/commands → commands}/index.d.ts +0 -0
  227. /package/dist/types/src/{cli/commands → commands}/util.d.ts +0 -0
  228. /package/dist/types/src/{cli/storage.d.ts → storage.d.ts} +0 -0
  229. /package/src/{cli/commands → commands}/disable.ts +0 -0
  230. /package/src/{cli/commands → commands}/enable.ts +0 -0
  231. /package/src/{cli/commands → commands}/index.ts +0 -0
  232. /package/src/{cli/commands → commands}/list.ts +0 -0
  233. /package/src/{cli/commands → commands}/util.ts +0 -0
  234. /package/src/{cli/storage.ts → storage.ts} +0 -0
@@ -5,10 +5,10 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
 
7
7
  import { Plugin } from '@dxos/app-framework';
8
- import { faker } from '@dxos/random';
8
+ import { random } from '@dxos/random';
9
9
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
10
 
11
- import { translations } from '../../translations';
11
+ import { translations } from '#translations';
12
12
 
13
13
  import { PluginDetail } from './PluginDetail';
14
14
 
@@ -29,9 +29,9 @@ type Story = StoryObj<typeof meta>;
29
29
  export const Default: Story = {
30
30
  args: {
31
31
  plugin: Plugin.define({
32
- id: 'example.com/plugin/test-plugin',
32
+ id: 'com.example.plugin.test',
33
33
  name: 'Test Plugin',
34
- description: faker.lorem.paragraphs(2),
34
+ description: random.lorem.paragraphs(2),
35
35
  icon: 'ph--bug--regular',
36
36
  iconHue: 'sky',
37
37
  homePage: 'https://example.com',
@@ -4,78 +4,193 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type Plugin } from '@dxos/app-framework';
8
- import { Icon, Input, Link, useTranslation } from '@dxos/react-ui';
9
- import { ScrollArea } from '@dxos/react-ui';
10
- import { getStyles, mx } from '@dxos/ui-theme';
7
+ import { type Registry, type Plugin, type PluginManager } from '@dxos/app-framework';
8
+ import { Button, Icon, Input, Link, ScrollArea, Select, useTranslation } from '@dxos/react-ui';
9
+ import { composable, composableProps, getStyles, mx } from '@dxos/ui-theme';
11
10
 
12
- import { meta } from '../../meta';
11
+ import { meta } from '#meta';
12
+
13
+ import { PluginFailureBadge } from '../PluginFailureBadge';
13
14
 
14
15
  export type PluginDetailProps = {
15
16
  plugin: Plugin.Plugin;
16
17
  enabled?: boolean;
17
18
  onEnabledChange?: (enabled: boolean) => void;
19
+ /**
20
+ * When provided, an Uninstall button is rendered. Leave undefined for core
21
+ * or non-removable plugins.
22
+ */
23
+ onUninstall?: () => void;
24
+ /**
25
+ * When provided, the plugin is not installed and an Install button is shown
26
+ * in place of the enable Switch.
27
+ */
28
+ onInstall?: () => void;
29
+ installing?: boolean;
30
+ /**
31
+ * When provided and the plugin is installed with a newer catalog version available,
32
+ * an Update button is shown in place of the enable Switch.
33
+ */
34
+ onUpdate?: () => void;
35
+ /** True when the catalog has a newer version than the one installed. */
36
+ hasUpdate?: boolean;
37
+ /** True while an in-flight update is running. Forces the Update button disabled state. */
38
+ updating?: boolean;
39
+ /** Available versions of this plugin from the catalog. When non-empty, a version picker is shown. */
40
+ versions?: readonly Registry.PluginVersion[];
41
+ /** Currently selected version tag in the picker. */
42
+ selectedVersionTag?: string;
43
+ /** Called when the user selects a different version in the picker. */
44
+ onVersionChange?: (tag: string) => void;
45
+ /** Called when the user clicks Install on the version picker. */
46
+ onInstallVersion?: () => void;
47
+ /** Currently installed version tag (used to mark the matching select option). */
48
+ installedVersionTag?: string;
49
+ /**
50
+ * Failure record for this plugin, if any. When present a warning badge is
51
+ * rendered next to the plugin name in the header.
52
+ */
53
+ failure?: PluginManager.PluginFailure;
18
54
  };
19
55
 
20
- export const PluginDetail = ({ plugin, enabled, onEnabledChange }: PluginDetailProps) => {
21
- const { t } = useTranslation(meta.id);
22
- const {
23
- id,
24
- name,
25
- description,
26
- homePage,
27
- source,
28
- screenshots,
29
- icon = 'ph--circle--regular',
30
- iconHue = 'neutral',
31
- } = plugin.meta;
32
- const styles = getStyles(iconHue);
56
+ export const PluginDetail = composable<HTMLDivElement, PluginDetailProps>(
57
+ (
58
+ {
59
+ plugin,
60
+ enabled,
61
+ onEnabledChange,
62
+ onUninstall,
63
+ onInstall,
64
+ installing,
65
+ onUpdate,
66
+ hasUpdate,
67
+ updating,
68
+ versions,
69
+ selectedVersionTag,
70
+ onVersionChange,
71
+ onInstallVersion,
72
+ installedVersionTag,
73
+ failure,
74
+ ...props
75
+ },
76
+ forwardedRef,
77
+ ) => {
78
+ const { t } = useTranslation(meta.id);
79
+ const {
80
+ id,
81
+ name,
82
+ description,
83
+ homePage,
84
+ source,
85
+ screenshots,
86
+ icon = 'ph--circle--regular',
87
+ iconHue = 'neutral',
88
+ } = plugin.meta;
89
+ const styles = getStyles(iconHue);
33
90
 
34
- return (
35
- <ScrollArea.Root orientation='vertical'>
36
- <ScrollArea.Viewport>
37
- <div role='none' className='dx-article grid grid-cols-[min-content_1fr] gap-4 p-4'>
38
- <div role='none'>
39
- <Icon classNames={mx('p-1 rounded-md', styles.fill, styles.surfaceText)} icon={icon} size={14} />
40
- </div>
41
- <div role='none' className='flex flex-col gap-6'>
42
- <div role='none' className='grid grid-cols-[1fr_min-content] gap-x-3 w-full pt-1'>
43
- <h2 className='text-xl'>{name}</h2>
44
- <Input.Root>
45
- <Input.Switch classNames='self-center' checked={enabled} onCheckedChange={onEnabledChange} />
46
- </Input.Root>
47
- <p className='pt-0.5 text-sm text-description'>{id}</p>
48
- </div>
49
- <div role='none'>
50
- <p className='text-description'>{description}</p>
91
+ return (
92
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' ref={forwardedRef}>
93
+ <ScrollArea.Viewport>
94
+ <div className='dx-document grid grid-cols-[min-content_1fr] gap-4 p-4'>
95
+ <div>
96
+ <Icon classNames={mx('p-1 rounded-md', styles.fill, styles.surfaceText)} icon={icon} size={14} />
51
97
  </div>
52
- {screenshots && screenshots.length > 0 && (
53
- <div role='none' className='flex flex-col gap-2'>
54
- <h2>Preview</h2>
55
- <img src={screenshots[0]} alt={name} className='aspect-video object-fit' />
56
- </div>
57
- )}
58
- <div role='none' className='flex flex-col gap-2'>
59
- <h2>Resources</h2>
60
- <div className='flex gap-2'>
61
- {homePage && (
62
- <Link href={homePage} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
63
- {t('home page label')}
64
- <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mx-1' />
65
- </Link>
98
+ <div className='flex flex-col gap-6'>
99
+ <div className='grid grid-cols-[1fr_min-content] gap-x-3 w-full pt-1'>
100
+ <div className='flex items-center gap-2'>
101
+ <h2 className='text-xl'>{name}</h2>
102
+ {failure && <PluginFailureBadge failure={failure} size={5} />}
103
+ </div>
104
+ {onInstall ? (
105
+ <Button density='fine' variant='primary' disabled={installing} onClick={onInstall}>
106
+ {installing ? t('installing.label') : t('install.label')}
107
+ </Button>
108
+ ) : (
109
+ <Input.Root>
110
+ <Input.Switch classNames='self-center' checked={enabled} onCheckedChange={onEnabledChange} />
111
+ </Input.Root>
66
112
  )}
113
+ <p className='pt-0.5 text-sm text-description'>{id}</p>
114
+ </div>
115
+ <div>
116
+ <p className='text-description'>{description}</p>
117
+ </div>
118
+ {screenshots && screenshots.length > 0 && (
119
+ <div className='flex flex-col gap-2'>
120
+ <h2>Preview</h2>
121
+ <img src={screenshots[0]} alt={name} className='aspect-video object-fit' />
122
+ </div>
123
+ )}
124
+ <div className='flex flex-col gap-2'>
125
+ <h2>Resources</h2>
126
+ <div className='flex gap-2'>
127
+ {homePage && (
128
+ <Link href={homePage} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
129
+ {t('home-page.label')}
130
+ <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mx-1' />
131
+ </Link>
132
+ )}
67
133
 
68
- {source && (
69
- <Link href={source} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
70
- {t('source label')}
71
- <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mx-1' />
72
- </Link>
73
- )}
134
+ {source && (
135
+ <Link href={source} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
136
+ {t('source.label')}
137
+ <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mx-1' />
138
+ </Link>
139
+ )}
140
+ </div>
74
141
  </div>
142
+ {versions && versions.length > 0 && (
143
+ <div className='flex flex-col gap-2'>
144
+ <h2>{t('versions.label')}</h2>
145
+ <div className='flex gap-2 items-center'>
146
+ <Select.Root value={selectedVersionTag} onValueChange={onVersionChange}>
147
+ <Select.TriggerButton classNames='min-w-32' />
148
+ <Select.Portal>
149
+ <Select.Content>
150
+ <Select.Viewport>
151
+ {versions.map((versionEntry) => (
152
+ <Select.Option key={versionEntry.tag} value={versionEntry.tag}>
153
+ {versionEntry.tag}
154
+ {installedVersionTag === versionEntry.tag ? ` (${t('installed.label')})` : ''}
155
+ </Select.Option>
156
+ ))}
157
+ </Select.Viewport>
158
+ </Select.Content>
159
+ </Select.Portal>
160
+ </Select.Root>
161
+ {onInstallVersion && (
162
+ <Button
163
+ density='fine'
164
+ variant='primary'
165
+ disabled={installing || selectedVersionTag === installedVersionTag}
166
+ onClick={onInstallVersion}
167
+ >
168
+ {installing ? t('installing.label') : t('install-version.label')}
169
+ </Button>
170
+ )}
171
+ </div>
172
+ </div>
173
+ )}
174
+ {(onUninstall || (hasUpdate && onUpdate) || updating) && (
175
+ <div className='flex gap-2'>
176
+ {updating ? (
177
+ <Button variant='primary' disabled>
178
+ {t('updating.label')}
179
+ </Button>
180
+ ) : hasUpdate && onUpdate ? (
181
+ <Button variant='primary' onClick={onUpdate}>
182
+ {t('update.label')}
183
+ </Button>
184
+ ) : null}
185
+ {onUninstall && <Button onClick={onUninstall}>{t('uninstall.label')}</Button>}
186
+ </div>
187
+ )}
75
188
  </div>
76
189
  </div>
77
- </div>
78
- </ScrollArea.Viewport>
79
- </ScrollArea.Root>
80
- );
81
- };
190
+ </ScrollArea.Viewport>
191
+ </ScrollArea.Root>
192
+ );
193
+ },
194
+ );
195
+
196
+ PluginDetail.displayName = 'PluginDetail';
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type PluginManager } from '@dxos/app-framework';
8
+ import { IconButton, Popover, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { meta } from '#meta';
11
+
12
+ // Mirrors react-ui `Icon`'s `size` prop literal subset; kept inline so we don't
13
+ // have to add `@dxos/ui-types` as a dep just for one type alias.
14
+ type IconSize = 4 | 5 | 6 | 8;
15
+
16
+ export type PluginFailureBadgeProps = {
17
+ failure: PluginManager.PluginFailure;
18
+ /** Optional override for the trigger icon's tailwind classes. */
19
+ iconClassNames?: string;
20
+ /** Visual size of the warning icon (passed through to react-ui `<Icon />`). */
21
+ size?: IconSize;
22
+ };
23
+
24
+ /**
25
+ * Compact warning glyph rendered next to a plugin's name when it has failed
26
+ * to load or activate. Clicking the icon opens a popover that names the phase
27
+ * (load / activation), the reason (timeout / error), and the underlying error
28
+ * message — enough for an operator to tell "remote host is offline" apart
29
+ * from "the plugin crashed".
30
+ */
31
+ export const PluginFailureBadge = ({ failure, iconClassNames, size = 4 }: PluginFailureBadgeProps) => {
32
+ const { t } = useTranslation(meta.id);
33
+
34
+ const phaseLabel = failure.phase === 'load' ? t('failure-phase-load.label') : t('failure-phase-activation.label');
35
+ const reasonLabel =
36
+ failure.reason === 'timeout' ? t('failure-reason-timeout.label') : t('failure-reason-error.label');
37
+
38
+ return (
39
+ <Popover.Root>
40
+ <Popover.Trigger asChild>
41
+ <IconButton
42
+ variant='ghost'
43
+ iconOnly
44
+ noTooltip
45
+ icon='ph--warning--bold'
46
+ size={size}
47
+ iconClassNames={iconClassNames}
48
+ label={t('failure-badge.label')}
49
+ data-testid={`pluginFailureBadge.${failure.id}`}
50
+ // Don't bubble up to row click handlers (e.g. the card's `onClick`
51
+ // that opens the details pane). The popover is the action here.
52
+ onClick={(event) => event.stopPropagation()}
53
+ />
54
+ </Popover.Trigger>
55
+ <Popover.Content>
56
+ <Popover.Viewport>
57
+ <div className='px-3 py-2 min-w-[18rem] max-w-[28rem] flex flex-col gap-1'>
58
+ <p className='font-medium text-sm'>
59
+ {t('failure-title.label', { phase: phaseLabel, reason: reasonLabel })}
60
+ </p>
61
+ <p className='text-description text-sm break-words'>{failure.error.message}</p>
62
+ </div>
63
+ </Popover.Viewport>
64
+ <Popover.Arrow />
65
+ </Popover.Content>
66
+ </Popover.Root>
67
+ );
68
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './PluginFailureBadge';
@@ -2,37 +2,100 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type MouseEvent, useCallback } from 'react';
5
+ import React, { type MouseEvent, useCallback, useMemo } from 'react';
6
6
 
7
- import { type Plugin } from '@dxos/app-framework';
8
- import { type ChromaticPalette, Icon, IconButton, Input, Link, ListItem, Tag, useTranslation } from '@dxos/react-ui';
7
+ import { type Plugin, type PluginManager } from '@dxos/app-framework';
8
+ import {
9
+ Button,
10
+ type ChromaticPalette,
11
+ Icon,
12
+ IconButton,
13
+ Input,
14
+ Link,
15
+ ListItem,
16
+ type NeutralPalette,
17
+ Tag,
18
+ useTranslation,
19
+ } from '@dxos/react-ui';
9
20
  import { mx } from '@dxos/ui-theme';
10
21
  import { getStyles } from '@dxos/ui-theme';
11
22
 
12
- import { meta } from '../../meta';
13
- import { type RegistryTagType } from '../../types';
23
+ import { meta } from '#meta';
24
+ import { type RegistryTagType } from '#types';
25
+
26
+ import { PluginFailureBadge } from '../PluginFailureBadge';
14
27
 
15
28
  export type PluginItemProps = {
16
29
  plugin: Plugin.Plugin;
30
+ /** Ids of plugins currently installed (loaded into the manager). */
17
31
  installed?: readonly string[];
32
+ /** Ids of plugins whose install is in flight. */
33
+ installing?: readonly string[];
18
34
  enabled?: readonly string[];
35
+ /**
36
+ * Derived tags (e.g. `registry`, `local`) to display alongside the plugin's own meta.tags.
37
+ * Not persisted to plugin meta; computed per-render by the container.
38
+ */
39
+ extraTags?: readonly string[];
19
40
  onClick?: (id: string) => void;
20
41
  onChange?: (id: string, enabled: boolean) => void;
42
+ /**
43
+ * Install handler. When provided and the plugin is not installed, an Install button
44
+ * is rendered in place of the enable switch.
45
+ */
46
+ onInstall?: (id: string) => void;
47
+ /**
48
+ * When true and plugin is installed, shows an Update button instead of the enable switch.
49
+ */
50
+ hasUpdate?: boolean;
51
+ /** Called when the user clicks the Update button. */
52
+ onUpdate?: (id: string) => void;
53
+ /** Ids of plugins whose update is currently in flight. */
54
+ updating?: readonly string[];
21
55
  hasSettings?: (id: string) => boolean;
22
56
  onSettings?: (id: string) => void;
57
+ /**
58
+ * Failure record for this plugin, if any. When present a warning badge is
59
+ * rendered next to the plugin name; clicking it opens a popover with the
60
+ * phase, reason, and error message.
61
+ */
62
+ failure?: PluginManager.PluginFailure;
23
63
  };
24
64
 
25
65
  export const PluginItem = ({
26
66
  plugin,
67
+ installed,
68
+ installing,
27
69
  enabled = [],
70
+ extraTags,
28
71
  onClick,
29
72
  onChange,
73
+ onInstall,
74
+ hasUpdate,
75
+ onUpdate,
76
+ updating,
30
77
  hasSettings: hasSettingsProp,
31
78
  onSettings,
79
+ failure,
32
80
  }: PluginItemProps) => {
33
81
  const { t } = useTranslation(meta.id);
34
82
  const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
83
+ const displayTags = useMemo(() => {
84
+ if (!extraTags || extraTags.length === 0) {
85
+ return tags ?? [];
86
+ }
87
+ const set = new Set<string>(tags ?? []);
88
+ for (const tag of extraTags) {
89
+ set.add(tag);
90
+ }
91
+ return Array.from(set);
92
+ }, [tags, extraTags]);
35
93
  const isEnabled = enabled.includes(id);
94
+ const isInstalled = installed ? installed.includes(id) : true;
95
+ const isInstalling = installing?.includes(id) ?? false;
96
+ const isUpdating = updating?.includes(id) ?? false;
97
+ const showInstallButton = !!onInstall && !isInstalled;
98
+ const showUpdateButton = !!onUpdate && isInstalled && !!hasUpdate;
36
99
  const inputId = `${id}-input`;
37
100
  const labelId = `${id}-label`;
38
101
  const descriptionId = `${id}-description`;
@@ -46,6 +109,22 @@ export const PluginItem = ({
46
109
  [id, isEnabled, onChange],
47
110
  );
48
111
 
112
+ const handleInstall = useCallback(
113
+ (event: MouseEvent) => {
114
+ event.stopPropagation();
115
+ onInstall?.(id);
116
+ },
117
+ [id, onInstall],
118
+ );
119
+
120
+ const handleUpdate = useCallback(
121
+ (event: MouseEvent) => {
122
+ event.stopPropagation();
123
+ onUpdate?.(id);
124
+ },
125
+ [id, onUpdate],
126
+ );
127
+
49
128
  const hasSettings = hasSettingsProp?.(id) ?? false;
50
129
  const handleSettings = useCallback(() => onSettings?.(id), [id, onSettings]);
51
130
  const styles = getStyles(iconHue);
@@ -58,7 +137,7 @@ export const PluginItem = ({
58
137
  labelId={labelId}
59
138
  data-testid={`pluginList.${id}`}
60
139
  aria-describedby={descriptionId}
61
- classNames={mx(gridCols, 'h-[12rem] w-full gap-3 pe-2 bg-modal-surface rounded-md overflow-hidden')}
140
+ classNames={mx(gridCols, 'h-[14rem] w-full gap-3 pe-2 bg-modal-surface rounded-md overflow-hidden')}
62
141
  >
63
142
  <div className={mx(gridRows, 'justify-center rounded-l-md', styles.surface)}>
64
143
  <Icon
@@ -70,8 +149,9 @@ export const PluginItem = ({
70
149
  </div>
71
150
 
72
151
  <div className={mx(gridRows)}>
73
- <div className='flex items-center overflow-hidden cursor-pointer' onClick={handleClick}>
152
+ <div className='flex items-center gap-2 overflow-hidden cursor-pointer' onClick={handleClick}>
74
153
  <span className='text-lg truncate'>{name ?? id}</span>
154
+ {failure && <PluginFailureBadge failure={failure} />}
75
155
  </div>
76
156
 
77
157
  <div>
@@ -79,7 +159,7 @@ export const PluginItem = ({
79
159
  </div>
80
160
 
81
161
  <div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
82
- {tags?.map((tag) => (
162
+ {displayTags.map((tag) => (
83
163
  <Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase font-thin'>
84
164
  {tag}
85
165
  </Tag>
@@ -91,7 +171,7 @@ export const PluginItem = ({
91
171
  aria-describedby={descriptionId}
92
172
  classNames='cursor-pointer'
93
173
  icon='ph--gear--regular'
94
- label={t('settings label')}
174
+ label={t('plugin-settings.label')}
95
175
  iconOnly
96
176
  size={4}
97
177
  onClick={handleSettings}
@@ -99,14 +179,34 @@ export const PluginItem = ({
99
179
  />
100
180
 
101
181
  <Link aria-describedby={descriptionId} classNames='text-description cursor-pointer' onClick={handleClick}>
102
- {t('details label')}
182
+ {t('details.label')}
103
183
  </Link>
104
184
 
105
185
  <div className='grow' />
106
186
  <div className='pe-1'>
107
- <Input.Root id={inputId}>
108
- <Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
109
- </Input.Root>
187
+ {isUpdating ? (
188
+ <Button aria-describedby={descriptionId} density='fine' variant='primary' disabled>
189
+ {t('updating.label')}
190
+ </Button>
191
+ ) : showUpdateButton ? (
192
+ <Button aria-describedby={descriptionId} density='fine' variant='primary' onClick={handleUpdate}>
193
+ {t('update.label')}
194
+ </Button>
195
+ ) : showInstallButton ? (
196
+ <Button
197
+ aria-describedby={descriptionId}
198
+ density='fine'
199
+ variant='primary'
200
+ disabled={isInstalling}
201
+ onClick={handleInstall}
202
+ >
203
+ {isInstalling ? t('installing.label') : t('install.label')}
204
+ </Button>
205
+ ) : (
206
+ <Input.Root id={inputId}>
207
+ <Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
208
+ </Input.Root>
209
+ )}
110
210
  </div>
111
211
  </div>
112
212
  </div>
@@ -114,11 +214,13 @@ export const PluginItem = ({
114
214
  );
115
215
  };
116
216
 
117
- const tagColors: Record<RegistryTagType, ChromaticPalette> = {
217
+ const tagColors: Record<RegistryTagType, ChromaticPalette | NeutralPalette> = {
118
218
  new: 'rose',
119
219
  beta: 'teal',
120
220
  labs: 'blue',
121
221
  popular: 'green',
122
222
  featured: 'pink',
123
223
  experimental: 'amber',
224
+ registry: 'indigo',
225
+ local: 'neutral',
124
226
  };
@@ -6,17 +6,17 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useState } from 'react';
7
7
 
8
8
  import { type Plugin, Plugin as PluginNS } from '@dxos/app-framework';
9
- import { faker } from '@dxos/random';
9
+ import { random } from '@dxos/random';
10
10
  import { ScrollArea } from '@dxos/react-ui';
11
11
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
12
  import { getHashHue } from '@dxos/ui-theme';
13
13
 
14
- import { translations } from '../../translations';
15
- import { RegistryTagType } from '../../types';
14
+ import { translations } from '#translations';
15
+ import { RegistryTagType } from '#types';
16
16
 
17
17
  import { PluginList } from './PluginList';
18
18
 
19
- faker.seed(1);
19
+ random.seed(1);
20
20
 
21
21
  const icons = [
22
22
  'ph--bug--regular',
@@ -29,17 +29,17 @@ const icons = [
29
29
 
30
30
  const DefaultStory = () => {
31
31
  const [plugins] = useState<Plugin.Plugin[]>(
32
- faker.helpers.multiple(
32
+ random.helpers.multiple(
33
33
  () =>
34
34
  PluginNS.define({
35
- id: `org.dxos.plugin.plugin-${faker.string.uuid()}`,
36
- name: `${faker.commerce.productName()}`,
37
- description: faker.lorem.sentences(Math.ceil(Math.random() * 3)),
38
- tags: faker.helpers.uniqueArray(RegistryTagType.literals as any, Math.floor(Math.random() * 3)),
39
- icon: faker.helpers.arrayElement(icons),
40
- iconHue: getHashHue(faker.string.uuid()),
41
- homePage: faker.datatype.boolean({ probability: 0.5 }) ? faker.internet.url() : undefined,
42
- source: faker.internet.url(),
35
+ id: `org.dxos.plugin.plugin-${random.string.uuid()}`,
36
+ name: `${random.commerce.productName()}`,
37
+ description: random.lorem.sentences(Math.ceil(Math.random() * 3)),
38
+ tags: random.helpers.uniqueArray(RegistryTagType.literals as any, Math.floor(Math.random() * 3)),
39
+ icon: random.helpers.arrayElement(icons),
40
+ iconHue: getHashHue(random.string.uuid()),
41
+ homePage: random.datatype.boolean({ probability: 0.5 }) ? random.internet.url() : undefined,
42
+ source: random.internet.url(),
43
43
  }).pipe(PluginNS.make)(),
44
44
  { count: 32 },
45
45
  ),
@@ -73,7 +73,10 @@ export default meta;
73
73
  type Story = StoryObj<typeof meta>;
74
74
 
75
75
  export const Default: Story = {
76
- decorators: [withTheme(), withLayout({ layout: 'column' })],
76
+ decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'bg-deck-surface' })],
77
+ parameters: {
78
+ layout: 'fullscreen',
79
+ },
77
80
  };
78
81
 
79
82
  export const FullScreen: Story = {