@dxos/plugin-registry 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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 (165) hide show
  1. package/dist/lib/browser/PluginArticle-MLBF7AFA.mjs +29 -0
  2. package/dist/lib/browser/PluginArticle-MLBF7AFA.mjs.map +7 -0
  3. package/dist/lib/browser/PluginRegistry-ENAI4SAY.mjs +77 -0
  4. package/dist/lib/browser/PluginRegistry-ENAI4SAY.mjs.map +7 -0
  5. package/dist/lib/browser/app-graph-builder-2HXD7SOS.mjs +167 -0
  6. package/dist/lib/browser/app-graph-builder-2HXD7SOS.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-TSNK3SAR.mjs +191 -0
  8. package/dist/lib/browser/chunk-TSNK3SAR.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-VOAOTNGS.mjs +19 -0
  10. package/dist/lib/browser/chunk-VOAOTNGS.mjs.map +7 -0
  11. package/dist/lib/browser/cli/index.mjs +224 -0
  12. package/dist/lib/browser/cli/index.mjs.map +7 -0
  13. package/dist/lib/browser/index.mjs +23 -29
  14. package/dist/lib/browser/index.mjs.map +4 -4
  15. package/dist/lib/browser/meta.json +1 -1
  16. package/dist/lib/browser/operation-resolver-7P3NNVSR.mjs +25 -0
  17. package/dist/lib/browser/operation-resolver-7P3NNVSR.mjs.map +7 -0
  18. package/dist/lib/browser/react-surface-DH25DIRH.mjs +85 -0
  19. package/dist/lib/browser/react-surface-DH25DIRH.mjs.map +7 -0
  20. package/dist/lib/node-esm/PluginArticle-TP2G6GGI.mjs +30 -0
  21. package/dist/lib/node-esm/PluginArticle-TP2G6GGI.mjs.map +7 -0
  22. package/dist/lib/node-esm/PluginRegistry-CWIZIQNX.mjs +78 -0
  23. package/dist/lib/node-esm/PluginRegistry-CWIZIQNX.mjs.map +7 -0
  24. package/dist/lib/node-esm/app-graph-builder-2LNPN72O.mjs +168 -0
  25. package/dist/lib/node-esm/app-graph-builder-2LNPN72O.mjs.map +7 -0
  26. package/dist/lib/node-esm/chunk-4XO63XB6.mjs +192 -0
  27. package/dist/lib/node-esm/chunk-4XO63XB6.mjs.map +7 -0
  28. package/dist/lib/node-esm/chunk-SALMIZ2W.mjs +21 -0
  29. package/dist/lib/node-esm/chunk-SALMIZ2W.mjs.map +7 -0
  30. package/dist/lib/node-esm/cli/index.mjs +225 -0
  31. package/dist/lib/node-esm/cli/index.mjs.map +7 -0
  32. package/dist/lib/node-esm/index.mjs +64 -0
  33. package/dist/lib/node-esm/index.mjs.map +7 -0
  34. package/dist/lib/node-esm/meta.json +1 -0
  35. package/dist/lib/node-esm/operation-resolver-B24QW6IF.mjs +26 -0
  36. package/dist/lib/node-esm/operation-resolver-B24QW6IF.mjs.map +7 -0
  37. package/dist/lib/node-esm/react-surface-3YBEVL65.mjs +86 -0
  38. package/dist/lib/node-esm/react-surface-3YBEVL65.mjs.map +7 -0
  39. package/dist/types/src/RegistryPlugin.d.ts +2 -1
  40. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  41. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
  42. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
  44. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/index.d.ts +3 -3
  46. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  47. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  48. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  49. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  50. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  51. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  52. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  53. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  54. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  55. package/dist/types/src/cli/commands/disable.d.ts +11 -0
  56. package/dist/types/src/cli/commands/disable.d.ts.map +1 -0
  57. package/dist/types/src/cli/commands/enable.d.ts +11 -0
  58. package/dist/types/src/cli/commands/enable.d.ts.map +1 -0
  59. package/dist/types/src/cli/commands/index.d.ts +3 -0
  60. package/dist/types/src/cli/commands/index.d.ts.map +1 -0
  61. package/dist/types/src/cli/commands/list.d.ts +7 -0
  62. package/dist/types/src/cli/commands/list.d.ts.map +1 -0
  63. package/dist/types/src/cli/commands/util.d.ts +11 -0
  64. package/dist/types/src/cli/commands/util.d.ts.map +1 -0
  65. package/dist/types/src/cli/index.d.ts +3 -0
  66. package/dist/types/src/cli/index.d.ts.map +1 -0
  67. package/dist/types/src/cli/plugin.d.ts +3 -0
  68. package/dist/types/src/cli/plugin.d.ts.map +1 -0
  69. package/dist/types/src/cli/storage.d.ts +18 -0
  70. package/dist/types/src/cli/storage.d.ts.map +1 -0
  71. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +9 -0
  72. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -0
  73. package/dist/types/src/components/{PluginDetail.stories.d.ts → PluginDetail/PluginDetail.stories.d.ts} +1 -1
  74. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -0
  75. package/dist/types/src/components/PluginDetail/index.d.ts +2 -0
  76. package/dist/types/src/components/PluginDetail/index.d.ts.map +1 -0
  77. package/dist/types/src/components/{PluginItem.d.ts → PluginList/PluginItem.d.ts} +2 -2
  78. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -0
  79. package/dist/types/src/components/{PluginList.d.ts → PluginList/PluginList.d.ts} +1 -1
  80. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -0
  81. package/dist/types/src/components/{PluginList.stories.d.ts → PluginList/PluginList.stories.d.ts} +2 -5
  82. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -0
  83. package/dist/types/src/components/PluginList/index.d.ts +2 -0
  84. package/dist/types/src/components/PluginList/index.d.ts.map +1 -0
  85. package/dist/types/src/components/index.d.ts +0 -1
  86. package/dist/types/src/components/index.d.ts.map +1 -1
  87. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts +7 -0
  88. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -0
  89. package/dist/types/src/containers/PluginArticle/index.d.ts +3 -0
  90. package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -0
  91. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +8 -0
  92. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +1 -0
  93. package/dist/types/src/containers/PluginRegistry/index.d.ts +3 -0
  94. package/dist/types/src/containers/PluginRegistry/index.d.ts.map +1 -0
  95. package/dist/types/src/containers/index.d.ts +4 -0
  96. package/dist/types/src/containers/index.d.ts.map +1 -0
  97. package/dist/types/src/meta.d.ts +2 -2
  98. package/dist/types/src/meta.d.ts.map +1 -1
  99. package/dist/types/src/types.d.ts +2 -0
  100. package/dist/types/src/types.d.ts.map +1 -1
  101. package/dist/types/tsconfig.tsbuildinfo +1 -1
  102. package/package.json +49 -33
  103. package/src/RegistryPlugin.tsx +10 -24
  104. package/src/capabilities/app-graph-builder/app-graph-builder.ts +137 -0
  105. package/src/capabilities/app-graph-builder/index.ts +7 -0
  106. package/src/capabilities/index.ts +3 -5
  107. package/src/capabilities/operation-resolver/index.ts +7 -0
  108. package/src/capabilities/operation-resolver/operation-resolver.ts +24 -0
  109. package/src/capabilities/react-surface/index.ts +7 -0
  110. package/src/capabilities/react-surface/react-surface.tsx +88 -0
  111. package/src/cli/commands/disable.ts +47 -0
  112. package/src/cli/commands/enable.ts +44 -0
  113. package/src/cli/commands/index.ts +14 -0
  114. package/src/cli/commands/list.ts +41 -0
  115. package/src/cli/commands/util.ts +24 -0
  116. package/src/cli/index.ts +6 -0
  117. package/src/cli/plugin.ts +17 -0
  118. package/src/cli/storage.ts +57 -0
  119. package/src/components/PluginDetail/PluginDetail.stories.tsx +44 -0
  120. package/src/components/PluginDetail/PluginDetail.tsx +81 -0
  121. package/src/components/PluginDetail/index.ts +5 -0
  122. package/src/components/PluginList/PluginItem.tsx +124 -0
  123. package/src/components/{PluginList.stories.tsx → PluginList/PluginList.stories.tsx} +27 -26
  124. package/src/components/{PluginList.tsx → PluginList/PluginList.tsx} +3 -5
  125. package/src/components/PluginList/index.ts +5 -0
  126. package/src/components/index.ts +0 -1
  127. package/src/containers/PluginArticle/PluginArticle.tsx +28 -0
  128. package/src/containers/PluginArticle/index.ts +7 -0
  129. package/src/containers/PluginRegistry/PluginRegistry.tsx +86 -0
  130. package/src/containers/PluginRegistry/index.ts +7 -0
  131. package/src/containers/index.ts +8 -0
  132. package/src/meta.ts +7 -2
  133. package/src/types.ts +4 -0
  134. package/dist/lib/browser/app-graph-builder-5BKKIS5T.mjs +0 -162
  135. package/dist/lib/browser/app-graph-builder-5BKKIS5T.mjs.map +0 -7
  136. package/dist/lib/browser/chunk-MWZPWWWF.mjs +0 -14
  137. package/dist/lib/browser/chunk-MWZPWWWF.mjs.map +0 -7
  138. package/dist/lib/browser/intent-resolver-RUD6WFGT.mjs +0 -23
  139. package/dist/lib/browser/intent-resolver-RUD6WFGT.mjs.map +0 -7
  140. package/dist/lib/browser/react-surface-FYRVOQGA.mjs +0 -335
  141. package/dist/lib/browser/react-surface-FYRVOQGA.mjs.map +0 -7
  142. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  143. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  144. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  145. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  146. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  147. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  148. package/dist/types/src/components/Icon.stories.d.ts +0 -14
  149. package/dist/types/src/components/Icon.stories.d.ts.map +0 -1
  150. package/dist/types/src/components/PluginDetail.d.ts +0 -9
  151. package/dist/types/src/components/PluginDetail.d.ts.map +0 -1
  152. package/dist/types/src/components/PluginDetail.stories.d.ts.map +0 -1
  153. package/dist/types/src/components/PluginItem.d.ts.map +0 -1
  154. package/dist/types/src/components/PluginList.d.ts.map +0 -1
  155. package/dist/types/src/components/PluginList.stories.d.ts.map +0 -1
  156. package/dist/types/src/components/RegistryContainer.d.ts +0 -7
  157. package/dist/types/src/components/RegistryContainer.d.ts.map +0 -1
  158. package/src/capabilities/app-graph-builder.ts +0 -156
  159. package/src/capabilities/intent-resolver.ts +0 -27
  160. package/src/capabilities/react-surface.tsx +0 -85
  161. package/src/components/Icon.stories.tsx +0 -110
  162. package/src/components/PluginDetail.stories.tsx +0 -45
  163. package/src/components/PluginDetail.tsx +0 -65
  164. package/src/components/PluginItem.tsx +0 -119
  165. package/src/components/RegistryContainer.tsx +0 -84
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as FileSystem from '@effect/platform/FileSystem';
6
+ import * as Path from '@effect/platform/Path';
7
+ import * as Effect from 'effect/Effect';
8
+ import * as Schema from 'effect/Schema';
9
+
10
+ import { DX_CONFIG, getProfilePath } from '@dxos/client-protocol';
11
+
12
+ // TODO(wittjosiah): Factor out to app-framework?
13
+
14
+ const PLUGINS_FILE = 'plugins.json';
15
+
16
+ const PluginsSchema = Schema.Array(Schema.String);
17
+
18
+ /**
19
+ * Get the path to the plugins configuration file for a profile.
20
+ */
21
+ const getPluginsPath = (profile: string): Effect.Effect<string, never, Path.Path> =>
22
+ Effect.gen(function* () {
23
+ const path = yield* Path.Path;
24
+ return path.join(getProfilePath(DX_CONFIG, profile), PLUGINS_FILE);
25
+ });
26
+
27
+ /**
28
+ * Load enabled plugins from profile storage.
29
+ * Returns the list of enabled plugin IDs, or an empty array if the file doesn't exist.
30
+ */
31
+ export const loadEnabledPlugins = Effect.fn(function* ({ profile }: { profile: string }) {
32
+ const fs = yield* FileSystem.FileSystem;
33
+ const pluginsPath = yield* getPluginsPath(profile);
34
+
35
+ const content = yield* fs
36
+ .readFileString(pluginsPath)
37
+ .pipe(Effect.catchTag('SystemError', () => Effect.succeed('[]')));
38
+
39
+ const parsed = yield* Schema.decodeUnknown(Schema.parseJson(PluginsSchema))(content).pipe(
40
+ Effect.catchAll(() => Effect.succeed([] as string[])),
41
+ );
42
+
43
+ return parsed;
44
+ });
45
+
46
+ /**
47
+ * Save enabled plugins to profile storage.
48
+ */
49
+ export const saveEnabledPlugins = Effect.fn(function* ({ profile, enabled }: { profile: string; enabled: string[] }) {
50
+ const fs = yield* FileSystem.FileSystem;
51
+ const pluginsPath = yield* getPluginsPath(profile);
52
+ const profileDir = getProfilePath(DX_CONFIG, profile);
53
+
54
+ yield* fs.makeDirectory(profileDir, { recursive: true });
55
+ const encoded = Schema.encodeSync(Schema.parseJson(PluginsSchema))(enabled);
56
+ yield* fs.writeFileString(pluginsPath, encoded + '\n');
57
+ });
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { Plugin } from '@dxos/app-framework';
8
+ import { faker } from '@dxos/random';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { translations } from '../../translations';
12
+
13
+ import { PluginDetail } from './PluginDetail';
14
+
15
+ const meta = {
16
+ title: 'plugins/plugin-registry/components/PluginDetail',
17
+ component: PluginDetail,
18
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
19
+ parameters: {
20
+ layout: 'fullscreen',
21
+ translations,
22
+ },
23
+ } satisfies Meta<typeof PluginDetail>;
24
+
25
+ export default meta;
26
+
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ plugin: Plugin.define({
32
+ id: 'example.com/plugin/test-plugin',
33
+ name: 'Test Plugin',
34
+ description: faker.lorem.paragraphs(2),
35
+ icon: 'ph--bug--regular',
36
+ iconHue: 'sky',
37
+ homePage: 'https://example.com',
38
+ source: 'https://github.com/example/test-plugin',
39
+ screenshots: [
40
+ 'https://media.gcflearnfree.org/content/55e073de7dd48174331f51b3_01_17_2014/getting_started_interactive2.png',
41
+ ],
42
+ }).pipe(Plugin.make)(),
43
+ },
44
+ };
@@ -0,0 +1,81 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
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';
11
+
12
+ import { meta } from '../../meta';
13
+
14
+ export type PluginDetailProps = {
15
+ plugin: Plugin.Plugin;
16
+ enabled?: boolean;
17
+ onEnabledChange?: (enabled: boolean) => void;
18
+ };
19
+
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);
33
+
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>
51
+ </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>
66
+ )}
67
+
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
+ )}
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </ScrollArea.Viewport>
79
+ </ScrollArea.Root>
80
+ );
81
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './PluginDetail';
@@ -0,0 +1,124 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type MouseEvent, useCallback } from 'react';
6
+
7
+ import { type Plugin } from '@dxos/app-framework';
8
+ import { type ChromaticPalette, Icon, IconButton, Input, Link, ListItem, Tag, useTranslation } from '@dxos/react-ui';
9
+ import { mx } from '@dxos/ui-theme';
10
+ import { getStyles } from '@dxos/ui-theme';
11
+
12
+ import { meta } from '../../meta';
13
+ import { type RegistryTagType } from '../../types';
14
+
15
+ export type PluginItemProps = {
16
+ plugin: Plugin.Plugin;
17
+ installed?: readonly string[];
18
+ enabled?: readonly string[];
19
+ onClick?: (id: string) => void;
20
+ onChange?: (id: string, enabled: boolean) => void;
21
+ hasSettings?: (id: string) => boolean;
22
+ onSettings?: (id: string) => void;
23
+ };
24
+
25
+ export const PluginItem = ({
26
+ plugin,
27
+ enabled = [],
28
+ onClick,
29
+ onChange,
30
+ hasSettings: hasSettingsProp,
31
+ onSettings,
32
+ }: PluginItemProps) => {
33
+ const { t } = useTranslation(meta.id);
34
+ const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
35
+ const isEnabled = enabled.includes(id);
36
+ const inputId = `${id}-input`;
37
+ const labelId = `${id}-label`;
38
+ const descriptionId = `${id}-description`;
39
+ const handleClick = useCallback(() => onClick?.(id), [id, onClick]);
40
+
41
+ const handleChange = useCallback(
42
+ (event: MouseEvent) => {
43
+ event.stopPropagation();
44
+ onChange?.(id, !isEnabled);
45
+ },
46
+ [id, isEnabled, onChange],
47
+ );
48
+
49
+ const hasSettings = hasSettingsProp?.(id) ?? false;
50
+ const handleSettings = useCallback(() => onSettings?.(id), [id, onSettings]);
51
+ const styles = getStyles(iconHue);
52
+ const gridCols = 'grid grid-cols-[5rem_1fr]';
53
+ const gridRows = 'grid grid-rows-[40px_1fr_min-content_40px]';
54
+
55
+ return (
56
+ <ListItem.Root
57
+ key={id}
58
+ labelId={labelId}
59
+ data-testid={`pluginList.${id}`}
60
+ aria-describedby={descriptionId}
61
+ classNames={mx(gridCols, 'h-[12rem] w-full gap-3 pe-2 bg-modal-surface rounded-md overflow-hidden')}
62
+ >
63
+ <div className={mx(gridRows, 'justify-center rounded-l-md', styles.surface)}>
64
+ <Icon
65
+ classNames={mx('row-start-2 cursor-pointer', styles.surfaceText)}
66
+ icon={icon}
67
+ size={14}
68
+ onClick={handleClick}
69
+ />
70
+ </div>
71
+
72
+ <div className={mx(gridRows)}>
73
+ <div className='flex items-center overflow-hidden cursor-pointer' onClick={handleClick}>
74
+ <span className='text-lg truncate'>{name ?? id}</span>
75
+ </div>
76
+
77
+ <div>
78
+ <p className={mx('text-description', 'line-clamp-4 min-w-0')}>{description}</p>
79
+ </div>
80
+
81
+ <div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
82
+ {tags?.map((tag) => (
83
+ <Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase font-thin'>
84
+ {tag}
85
+ </Tag>
86
+ ))}
87
+ </div>
88
+
89
+ <div className='flex gap-2 items-center text-sm'>
90
+ <IconButton
91
+ aria-describedby={descriptionId}
92
+ classNames='cursor-pointer'
93
+ icon='ph--gear--regular'
94
+ label={t('settings label')}
95
+ iconOnly
96
+ size={4}
97
+ onClick={handleSettings}
98
+ disabled={!hasSettings}
99
+ />
100
+
101
+ <Link aria-describedby={descriptionId} classNames='text-description cursor-pointer' onClick={handleClick}>
102
+ {t('details label')}
103
+ </Link>
104
+
105
+ <div className='grow' />
106
+ <div className='pe-1'>
107
+ <Input.Root id={inputId}>
108
+ <Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
109
+ </Input.Root>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </ListItem.Root>
114
+ );
115
+ };
116
+
117
+ const tagColors: Record<RegistryTagType, ChromaticPalette> = {
118
+ new: 'rose',
119
+ beta: 'teal',
120
+ labs: 'blue',
121
+ popular: 'green',
122
+ featured: 'pink',
123
+ experimental: 'amber',
124
+ };
@@ -5,13 +5,14 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useState } from 'react';
7
7
 
8
- import { type Plugin, definePlugin } from '@dxos/app-framework';
8
+ import { type Plugin, Plugin as PluginNS } from '@dxos/app-framework';
9
9
  import { faker } from '@dxos/random';
10
- import { type ThemedClassName } from '@dxos/react-ui';
11
- import { withTheme } from '@dxos/react-ui/testing';
12
- import { mx } from '@dxos/react-ui-theme';
10
+ import { ScrollArea } from '@dxos/react-ui';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
+ import { getHashHue } from '@dxos/ui-theme';
13
13
 
14
- import { translations } from '../translations';
14
+ import { translations } from '../../translations';
15
+ import { RegistryTagType } from '../../types';
15
16
 
16
17
  import { PluginList } from './PluginList';
17
18
 
@@ -26,24 +27,21 @@ const icons = [
26
27
  'ph--github-logo--regular',
27
28
  ];
28
29
 
29
- const DefaultStory = ({ classNames }: ThemedClassName<{}>) => {
30
- const [plugins] = useState<Plugin[]>(
30
+ const DefaultStory = () => {
31
+ const [plugins] = useState<Plugin.Plugin[]>(
31
32
  faker.helpers.multiple(
32
- definePlugin(
33
- {
33
+ () =>
34
+ PluginNS.define({
34
35
  id: `dxos.org/plugin/plugin-${faker.string.uuid()}`,
35
36
  name: `${faker.commerce.productName()}`,
36
37
  description: faker.lorem.sentences(Math.ceil(Math.random() * 3)),
37
- tags: faker.datatype.boolean({ probability: 0.6 })
38
- ? [faker.helpers.arrayElement(['labs', 'beta', 'alpha', 'stable', 'new', '新発売'])]
39
- : undefined,
38
+ tags: faker.helpers.uniqueArray(RegistryTagType.literals as any, Math.floor(Math.random() * 3)),
40
39
  icon: faker.helpers.arrayElement(icons),
40
+ iconHue: getHashHue(faker.string.uuid()),
41
41
  homePage: faker.datatype.boolean({ probability: 0.5 }) ? faker.internet.url() : undefined,
42
42
  source: faker.internet.url(),
43
- },
44
- () => [],
45
- ),
46
- { count: 16 },
43
+ }).pipe(PluginNS.make)(),
44
+ { count: 32 },
47
45
  ),
48
46
  );
49
47
  const [enabled, setEnabled] = useState<string[]>([]);
@@ -53,19 +51,19 @@ const DefaultStory = ({ classNames }: ThemedClassName<{}>) => {
53
51
  };
54
52
 
55
53
  return (
56
- <div className={mx('flex overflow-hidden', classNames)}>
57
- <PluginList plugins={plugins} enabled={enabled} onChange={handleChange} hasSettings={() => true} />
58
- </div>
54
+ <ScrollArea.Root orientation='vertical'>
55
+ <ScrollArea.Viewport>
56
+ <PluginList plugins={plugins} enabled={enabled} onChange={handleChange} hasSettings={() => true} />
57
+ </ScrollArea.Viewport>
58
+ </ScrollArea.Root>
59
59
  );
60
60
  };
61
61
 
62
62
  const meta = {
63
- title: 'plugins/plugin-registry/PluginList',
63
+ title: 'plugins/plugin-registry/components/PluginList',
64
64
  component: PluginList,
65
65
  render: DefaultStory,
66
- decorators: [withTheme],
67
66
  parameters: {
68
- layout: 'column',
69
67
  translations,
70
68
  },
71
69
  } satisfies Meta<typeof PluginList>;
@@ -74,10 +72,13 @@ export default meta;
74
72
 
75
73
  type Story = StoryObj<typeof meta>;
76
74
 
77
- export const Default: Story = {};
75
+ export const Default: Story = {
76
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
77
+ };
78
78
 
79
- export const Column: Story = {
80
- args: {
81
- classNames: 'w-[30rem]',
79
+ export const FullScreen: Story = {
80
+ decorators: [withTheme(), withLayout({ scroll: true })],
81
+ parameters: {
82
+ layout: 'fullscreen',
82
83
  },
83
84
  };
@@ -10,16 +10,14 @@ import { List } from '@dxos/react-ui';
10
10
  import { PluginItem, type PluginItemProps } from './PluginItem';
11
11
 
12
12
  export type PluginListProps = Omit<PluginItemProps, 'plugin'> & {
13
- plugins?: readonly Plugin[];
13
+ plugins?: readonly Plugin.Plugin[];
14
14
  };
15
15
 
16
16
  export const PluginList = ({ plugins = [], ...props }: PluginListProps) => {
17
17
  return (
18
- <List classNames='grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] auto-rows-[12rem] gap-3 p-3'>
18
+ <List classNames='grid grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] auto-rows-[max-content] gap-4 p-4'>
19
19
  {plugins.map((plugin) => (
20
- <div key={plugin.meta.id} className='flex'>
21
- <PluginItem plugin={plugin} {...props} />
22
- </div>
20
+ <PluginItem key={plugin.meta.id} plugin={plugin} {...props} />
23
21
  ))}
24
22
  </List>
25
23
  );
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './PluginList';
@@ -4,4 +4,3 @@
4
4
 
5
5
  export * from './PluginDetail';
6
6
  export * from './PluginList';
7
- export * from './RegistryContainer';
@@ -0,0 +1,28 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { type Plugin } from '@dxos/app-framework';
8
+ import { usePluginManager } from '@dxos/app-framework/ui';
9
+ import { runAndForwardErrors } from '@dxos/effect';
10
+
11
+ import { PluginDetail } from '../../components';
12
+
13
+ // TODO(burdon): Convert to ECHO type.
14
+ export type PluginArticleProps = { subject: Plugin.Plugin };
15
+
16
+ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
17
+ const manager = usePluginManager();
18
+ const enabled = manager.getEnabled().includes(plugin.meta.id);
19
+ const handleEnableChange = useCallback(
20
+ (enabled: boolean) =>
21
+ enabled
22
+ ? runAndForwardErrors(manager.enable(plugin.meta.id))
23
+ : runAndForwardErrors(manager.disable(plugin.meta.id)),
24
+ [manager, plugin.meta.id],
25
+ );
26
+
27
+ return <PluginDetail plugin={plugin} enabled={enabled} onEnabledChange={handleEnableChange} />;
28
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { PluginArticle } from './PluginArticle';
6
+
7
+ export default PluginArticle;
@@ -0,0 +1,86 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
7
+ import React, { useCallback, useMemo } from 'react';
8
+
9
+ import { type Plugin } from '@dxos/app-framework';
10
+ import { useCapabilities, useOperationInvoker, usePluginManager } from '@dxos/app-framework/ui';
11
+ import { AppCapabilities, LayoutOperation, SettingsOperation } from '@dxos/app-toolkit';
12
+ import { runAndForwardErrors } from '@dxos/effect';
13
+ import { ObservabilityOperation } from '@dxos/plugin-observability/types';
14
+ import { ScrollArea } from '@dxos/react-ui';
15
+
16
+ import { PluginList } from '../../components';
17
+
18
+ const sortByPluginMeta = ({ meta: { name: a = '' } }: Plugin.Plugin, { meta: { name: b = '' } }: Plugin.Plugin) =>
19
+ a.localeCompare(b);
20
+
21
+ export type PluginRegistryProps = {
22
+ id: string;
23
+ plugins: Plugin.Plugin[];
24
+ };
25
+
26
+ export const PluginRegistry = ({ id, plugins: pluginsProp }: PluginRegistryProps) => {
27
+ const manager = usePluginManager();
28
+ const { invoke, invokePromise } = useOperationInvoker();
29
+ const plugins = useMemo(() => pluginsProp.sort(sortByPluginMeta), [pluginsProp]);
30
+ const enabled = useAtomValue(manager.enabled);
31
+ const allSettings = useCapabilities(AppCapabilities.Settings);
32
+
33
+ // TODO(wittjosiah): Factor out to an intent?
34
+ const handleChange = useCallback(
35
+ (id: string, enabled: boolean) =>
36
+ Effect.gen(function* () {
37
+ if (enabled) {
38
+ yield* manager.enable(id);
39
+ } else {
40
+ yield* manager.disable(id);
41
+ }
42
+
43
+ yield* invoke(ObservabilityOperation.SendEvent, {
44
+ name: 'plugins.toggle',
45
+ properties: {
46
+ plugin: id,
47
+ enabled,
48
+ },
49
+ });
50
+ }).pipe(runAndForwardErrors),
51
+ [invoke, manager],
52
+ );
53
+
54
+ const handleClick = useCallback(
55
+ (pluginId: string) =>
56
+ invokePromise(LayoutOperation.Open, {
57
+ // TODO(wittjosiah): `/` currently is not supported in ids.
58
+ subject: [pluginId.replaceAll('/', ':')],
59
+ pivotId: id,
60
+ positioning: 'end',
61
+ }),
62
+ [invokePromise, id],
63
+ );
64
+
65
+ const hasSettings = useCallback((pluginId: string) => allSettings.some((s) => s.prefix === pluginId), [allSettings]);
66
+
67
+ const handleSettings = useCallback(
68
+ (pluginId: string) => invokePromise(SettingsOperation.Open, { plugin: pluginId }),
69
+ [invokePromise],
70
+ );
71
+
72
+ return (
73
+ <ScrollArea.Root orientation='vertical'>
74
+ <ScrollArea.Viewport>
75
+ <PluginList
76
+ plugins={plugins}
77
+ enabled={enabled}
78
+ onClick={handleClick}
79
+ onChange={handleChange}
80
+ hasSettings={hasSettings}
81
+ onSettings={handleSettings}
82
+ />
83
+ </ScrollArea.Viewport>
84
+ </ScrollArea.Root>
85
+ );
86
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { PluginRegistry } from './PluginRegistry';
6
+
7
+ export default PluginRegistry;
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type ComponentType, lazy } from 'react';
6
+
7
+ export const PluginArticle: ComponentType<any> = lazy(() => import('./PluginArticle'));
8
+ export const PluginRegistry: ComponentType<any> = lazy(() => import('./PluginRegistry'));
package/src/meta.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type PluginMeta } from '@dxos/app-framework';
5
+ import { type Plugin } from '@dxos/app-framework';
6
+ import { trim } from '@dxos/util';
6
7
 
7
8
  // TODO(wittjosiah): Deck does not currently support `/` in ids.
8
9
  // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
@@ -10,7 +11,11 @@ import { type PluginMeta } from '@dxos/app-framework';
10
11
  export const REGISTRY_ID = '!dxos:plugin-registry';
11
12
  export const REGISTRY_KEY = 'plugin-registry';
12
13
 
13
- export const meta: PluginMeta = {
14
+ export const meta: Plugin.Meta = {
14
15
  id: 'dxos.org/plugin/registry',
15
16
  name: 'Plugins',
17
+ description: trim`
18
+ Plugin management system for discovering, installing, and configuring workspace extensions.
19
+ Browse available plugins and customize your workspace capabilities.
20
+ `,
16
21
  };
package/src/types.ts CHANGED
@@ -11,3 +11,7 @@ export const RegistrySettingsSchema = Schema.mutable(
11
11
  );
12
12
 
13
13
  export type RegistrySettings = Schema.Schema.Type<typeof RegistrySettingsSchema>;
14
+
15
+ export const RegistryTagType = Schema.Literal('new', 'beta', 'labs', 'popular', 'featured', 'experimental');
16
+
17
+ export type RegistryTagType = Schema.Schema.Type<typeof RegistryTagType>;