@dxos/plugin-registry 0.8.4-main.b97322e → 0.8.4-main.bc674ce

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 (120) hide show
  1. package/dist/lib/browser/app-graph-builder-P3ZGGFAR.mjs +166 -0
  2. package/dist/lib/browser/app-graph-builder-P3ZGGFAR.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-VOAOTNGS.mjs +19 -0
  4. package/dist/lib/browser/chunk-VOAOTNGS.mjs.map +7 -0
  5. package/dist/lib/browser/cli/index.mjs +223 -0
  6. package/dist/lib/browser/cli/index.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +22 -31
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/operation-resolver-P5PKLPRT.mjs +24 -0
  11. package/dist/lib/browser/operation-resolver-P5PKLPRT.mjs.map +7 -0
  12. package/dist/lib/browser/react-surface-IIM6T7IA.mjs +337 -0
  13. package/dist/lib/browser/react-surface-IIM6T7IA.mjs.map +7 -0
  14. package/dist/lib/node-esm/app-graph-builder-56JXLOGZ.mjs +167 -0
  15. package/dist/lib/node-esm/app-graph-builder-56JXLOGZ.mjs.map +7 -0
  16. package/dist/lib/node-esm/chunk-SALMIZ2W.mjs +21 -0
  17. package/dist/lib/node-esm/chunk-SALMIZ2W.mjs.map +7 -0
  18. package/dist/lib/node-esm/cli/index.mjs +224 -0
  19. package/dist/lib/node-esm/cli/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/index.mjs +63 -0
  21. package/dist/lib/node-esm/index.mjs.map +7 -0
  22. package/dist/lib/node-esm/meta.json +1 -0
  23. package/dist/lib/node-esm/operation-resolver-3ZB5W3T3.mjs +25 -0
  24. package/dist/lib/node-esm/operation-resolver-3ZB5W3T3.mjs.map +7 -0
  25. package/dist/lib/node-esm/react-surface-F67P7RC3.mjs +338 -0
  26. package/dist/lib/node-esm/react-surface-F67P7RC3.mjs.map +7 -0
  27. package/dist/types/src/RegistryPlugin.d.ts +2 -1
  28. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  29. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts +6 -0
  30. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +1 -0
  31. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +3 -0
  32. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +1 -0
  33. package/dist/types/src/capabilities/index.d.ts +3 -3
  34. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  35. package/dist/types/src/capabilities/operation-resolver/index.d.ts +3 -0
  36. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +5 -0
  38. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +1 -0
  39. package/dist/types/src/capabilities/react-surface/index.d.ts +3 -0
  40. package/dist/types/src/capabilities/react-surface/index.d.ts.map +1 -0
  41. package/dist/types/src/capabilities/react-surface/react-surface.d.ts +5 -0
  42. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +1 -0
  43. package/dist/types/src/cli/commands/disable.d.ts +11 -0
  44. package/dist/types/src/cli/commands/disable.d.ts.map +1 -0
  45. package/dist/types/src/cli/commands/enable.d.ts +11 -0
  46. package/dist/types/src/cli/commands/enable.d.ts.map +1 -0
  47. package/dist/types/src/cli/commands/index.d.ts +3 -0
  48. package/dist/types/src/cli/commands/index.d.ts.map +1 -0
  49. package/dist/types/src/cli/commands/list.d.ts +7 -0
  50. package/dist/types/src/cli/commands/list.d.ts.map +1 -0
  51. package/dist/types/src/cli/commands/util.d.ts +11 -0
  52. package/dist/types/src/cli/commands/util.d.ts.map +1 -0
  53. package/dist/types/src/cli/index.d.ts +3 -0
  54. package/dist/types/src/cli/index.d.ts.map +1 -0
  55. package/dist/types/src/cli/plugin.d.ts +3 -0
  56. package/dist/types/src/cli/plugin.d.ts.map +1 -0
  57. package/dist/types/src/cli/storage.d.ts +18 -0
  58. package/dist/types/src/cli/storage.d.ts.map +1 -0
  59. package/dist/types/src/components/PluginDetail.d.ts +3 -3
  60. package/dist/types/src/components/PluginDetail.d.ts.map +1 -1
  61. package/dist/types/src/components/PluginDetail.stories.d.ts +29 -5
  62. package/dist/types/src/components/PluginDetail.stories.d.ts.map +1 -1
  63. package/dist/types/src/components/PluginItem.d.ts +2 -2
  64. package/dist/types/src/components/PluginItem.d.ts.map +1 -1
  65. package/dist/types/src/components/PluginList.d.ts +1 -1
  66. package/dist/types/src/components/PluginList.d.ts.map +1 -1
  67. package/dist/types/src/components/PluginList.stories.d.ts +31 -8
  68. package/dist/types/src/components/PluginList.stories.d.ts.map +1 -1
  69. package/dist/types/src/components/RegistryContainer.d.ts +4 -3
  70. package/dist/types/src/components/RegistryContainer.d.ts.map +1 -1
  71. package/dist/types/src/meta.d.ts +2 -3
  72. package/dist/types/src/meta.d.ts.map +1 -1
  73. package/dist/types/src/types.d.ts +3 -1
  74. package/dist/types/src/types.d.ts.map +1 -1
  75. package/dist/types/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +49 -34
  77. package/src/RegistryPlugin.tsx +9 -25
  78. package/src/capabilities/app-graph-builder/app-graph-builder.ts +136 -0
  79. package/src/capabilities/app-graph-builder/index.ts +7 -0
  80. package/src/capabilities/index.ts +3 -5
  81. package/src/capabilities/operation-resolver/index.ts +7 -0
  82. package/src/capabilities/operation-resolver/operation-resolver.ts +23 -0
  83. package/src/capabilities/react-surface/index.ts +7 -0
  84. package/src/capabilities/react-surface/react-surface.tsx +99 -0
  85. package/src/cli/commands/disable.ts +47 -0
  86. package/src/cli/commands/enable.ts +44 -0
  87. package/src/cli/commands/index.ts +14 -0
  88. package/src/cli/commands/list.ts +41 -0
  89. package/src/cli/commands/util.ts +24 -0
  90. package/src/cli/index.ts +6 -0
  91. package/src/cli/plugin.ts +16 -0
  92. package/src/cli/storage.ts +57 -0
  93. package/src/components/PluginDetail.stories.tsx +21 -23
  94. package/src/components/PluginDetail.tsx +55 -39
  95. package/src/components/PluginItem.tsx +55 -54
  96. package/src/components/PluginList.stories.tsx +32 -37
  97. package/src/components/PluginList.tsx +3 -5
  98. package/src/components/RegistryContainer.tsx +43 -44
  99. package/src/meta.ts +8 -5
  100. package/src/types.ts +5 -1
  101. package/dist/lib/browser/app-graph-builder-LKQBQDUA.mjs +0 -161
  102. package/dist/lib/browser/app-graph-builder-LKQBQDUA.mjs.map +0 -7
  103. package/dist/lib/browser/chunk-AEBASNOI.mjs +0 -16
  104. package/dist/lib/browser/chunk-AEBASNOI.mjs.map +0 -7
  105. package/dist/lib/browser/intent-resolver-W67CIJI3.mjs +0 -23
  106. package/dist/lib/browser/intent-resolver-W67CIJI3.mjs.map +0 -7
  107. package/dist/lib/browser/react-surface-3GNB7AOY.mjs +0 -333
  108. package/dist/lib/browser/react-surface-3GNB7AOY.mjs.map +0 -7
  109. package/dist/types/src/capabilities/app-graph-builder.d.ts +0 -4
  110. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +0 -1
  111. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  112. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  113. package/dist/types/src/capabilities/react-surface.d.ts +0 -4
  114. package/dist/types/src/capabilities/react-surface.d.ts.map +0 -1
  115. package/dist/types/src/components/Icon.stories.d.ts +0 -6
  116. package/dist/types/src/components/Icon.stories.d.ts.map +0 -1
  117. package/src/capabilities/app-graph-builder.ts +0 -155
  118. package/src/capabilities/intent-resolver.ts +0 -27
  119. package/src/capabilities/react-surface.tsx +0 -90
  120. package/src/components/Icon.stories.tsx +0 -108
@@ -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
+ });
@@ -2,44 +2,42 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type StoryObj, type Meta } from '@storybook/react-vite';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
 
7
+ import { Plugin } from '@dxos/app-framework';
9
8
  import { faker } from '@dxos/random';
10
- import { withLayout, withTheme } from '@dxos/storybook-utils';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
10
 
12
- import { PluginDetail, type PluginDetailProps } from './PluginDetail';
13
11
  import { translations } from '../translations';
14
12
 
15
- const meta: Meta<PluginDetailProps> = {
13
+ import { PluginDetail } from './PluginDetail';
14
+
15
+ const meta = {
16
16
  title: 'plugins/plugin-registry/PluginDetail',
17
17
  component: PluginDetail,
18
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: 'justify-center' })],
18
+ decorators: [withTheme, withLayout({ layout: 'column' })],
19
19
  parameters: {
20
20
  translations,
21
21
  },
22
- };
22
+ } satisfies Meta<typeof PluginDetail>;
23
23
 
24
24
  export default meta;
25
25
 
26
- type Story = StoryObj<PluginDetailProps>;
26
+ type Story = StoryObj<typeof meta>;
27
27
 
28
28
  export const Default: Story = {
29
29
  args: {
30
- plugin: {
31
- meta: {
32
- id: 'example.com/plugin/test-plugin',
33
- name: 'Test Plugin',
34
- description: faker.lorem.paragraphs(2),
35
- icon: 'ph--bug--regular',
36
- homePage: 'https://example.com',
37
- source: 'https://github.com/example/test-plugin',
38
- screenshots: [
39
- 'https://media.gcflearnfree.org/content/55e073de7dd48174331f51b3_01_17_2014/getting_started_interactive2.png',
40
- ],
41
- },
42
- modules: [],
43
- },
30
+ plugin: Plugin.define({
31
+ id: 'example.com/plugin/test-plugin',
32
+ name: 'Test Plugin',
33
+ description: faker.lorem.paragraphs(2),
34
+ icon: 'ph--bug--regular',
35
+ iconHue: 'sky',
36
+ homePage: 'https://example.com',
37
+ source: 'https://github.com/example/test-plugin',
38
+ screenshots: [
39
+ 'https://media.gcflearnfree.org/content/55e073de7dd48174331f51b3_01_17_2014/getting_started_interactive2.png',
40
+ ],
41
+ }).pipe(Plugin.make)(),
44
42
  },
45
43
  };
@@ -6,58 +6,74 @@ import React from 'react';
6
6
 
7
7
  import { type Plugin } from '@dxos/app-framework';
8
8
  import { Icon, Input, Link, useTranslation } from '@dxos/react-ui';
9
- import { DeprecatedFormContainer } from '@dxos/react-ui-form';
9
+ import { Layout } from '@dxos/react-ui-mosaic';
10
+ import { getStyles, mx } from '@dxos/ui-theme';
10
11
 
11
- import { REGISTRY_PLUGIN } from '../meta';
12
+ import { meta } from '../meta';
12
13
 
13
14
  export type PluginDetailProps = {
14
- plugin: Plugin;
15
+ plugin: Plugin.Plugin;
15
16
  enabled?: boolean;
16
- onEnable?: () => void;
17
+ onEnabledChange?: (enabled: boolean) => void;
17
18
  };
18
19
 
19
- export const PluginDetail = ({ plugin, enabled, onEnable }: PluginDetailProps) => {
20
- const { t } = useTranslation(REGISTRY_PLUGIN);
21
- const { id, name, description, homePage, source, screenshots } = plugin.meta;
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);
22
33
 
23
34
  return (
24
- <DeprecatedFormContainer classNames='gap-6'>
25
- <div role='none' className='flex justify-between items-center'>
26
- <p>{id}</p>
27
- <Input.Root>
28
- <Input.Switch classNames='self-center' checked={enabled} onClick={onEnable} />
29
- </Input.Root>
30
- </div>
31
-
32
- {screenshots && screenshots.length > 0 && (
35
+ <Layout.Container scrollable>
36
+ <div role='none' className='grid grid-cols-[min-content_1fr] gap-4 container-max-width p-4'>
33
37
  <div role='none'>
34
- <h1 className='text-lg mb-2'>Preview</h1>
35
- <img src={screenshots[0]} alt={name} className='aspect-video object-fit' />
38
+ <Icon classNames={mx('p-1 rounded', styles.bg, styles.icon)} icon={icon} size={14} />
36
39
  </div>
37
- )}
38
-
39
- <div role='none'>
40
- <p>{description}</p>
41
- </div>
42
-
43
- <div role='none'>
44
- <h1 className='text-lg mb-2'>Resources</h1>
45
- <div className='flex gap-2'>
46
- {homePage && (
47
- <Link href={homePage} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
48
- {t('home page label')}
49
- <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mli-1' />
50
- </Link>
40
+ <div role='none' className='flex flex-col gap-6'>
41
+ <div role='none' className='grid grid-cols-[1fr_min-content] gap-x-3 is-full pbs-1'>
42
+ <h2 className='text-xl'>{name}</h2>
43
+ <Input.Root>
44
+ <Input.Switch classNames='self-center' checked={enabled} onCheckedChange={onEnabledChange} />
45
+ </Input.Root>
46
+ <p className='pbs-0.5 text-sm text-description'>{id}</p>
47
+ </div>
48
+ <div role='none'>
49
+ <p className='text-description'>{description}</p>
50
+ </div>
51
+ {screenshots && screenshots.length > 0 && (
52
+ <div role='none' className='flex flex-col gap-2'>
53
+ <h2>Preview</h2>
54
+ <img src={screenshots[0]} alt={name} className='aspect-video object-fit' />
55
+ </div>
51
56
  )}
57
+ <div role='none' className='flex flex-col gap-2'>
58
+ <h2>Resources</h2>
59
+ <div className='flex gap-2'>
60
+ {homePage && (
61
+ <Link href={homePage} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
62
+ {t('home page label')}
63
+ <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mli-1' />
64
+ </Link>
65
+ )}
52
66
 
53
- {source && (
54
- <Link href={source} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
55
- {t('source label')}
56
- <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mli-1' />
57
- </Link>
58
- )}
67
+ {source && (
68
+ <Link href={source} target='_blank' rel='noreferrer' classNames='text-sm text-description'>
69
+ {t('source label')}
70
+ <Icon icon='ph--arrow-square-out--bold' size={3} classNames='inline-block leading-none mli-1' />
71
+ </Link>
72
+ )}
73
+ </div>
74
+ </div>
59
75
  </div>
60
76
  </div>
61
- </DeprecatedFormContainer>
77
+ </Layout.Container>
62
78
  );
63
79
  };
@@ -5,13 +5,15 @@
5
5
  import React, { type MouseEvent, useCallback } from 'react';
6
6
 
7
7
  import { type Plugin } from '@dxos/app-framework';
8
- import { Icon, IconButton, Input, Link, ListItem, Tag, useTranslation } from '@dxos/react-ui';
9
- import { descriptionText, mx } from '@dxos/react-ui-theme';
8
+ import { type ChromaticPalette, Icon, IconButton, Input, Link, ListItem, Tag, useTranslation } from '@dxos/react-ui';
9
+ import { descriptionText, mx } from '@dxos/ui-theme';
10
+ import { getStyles } from '@dxos/ui-theme';
10
11
 
11
- import { REGISTRY_PLUGIN } from '../meta';
12
+ import { meta } from '../meta';
13
+ import { type RegistryTagType } from '../types';
12
14
 
13
15
  export type PluginItemProps = {
14
- plugin: Plugin;
16
+ plugin: Plugin.Plugin;
15
17
  installed?: readonly string[];
16
18
  enabled?: readonly string[];
17
19
  onClick?: (id: string) => void;
@@ -25,11 +27,11 @@ export const PluginItem = ({
25
27
  enabled = [],
26
28
  onClick,
27
29
  onChange,
28
- hasSettings: _hasSettings,
30
+ hasSettings: hasSettingsProp,
29
31
  onSettings,
30
32
  }: PluginItemProps) => {
31
- const { t } = useTranslation(REGISTRY_PLUGIN);
32
- const { id, name, description, tags, icon = 'ph--circle--regular' } = plugin.meta;
33
+ const { t } = useTranslation(meta.id);
34
+ const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
33
35
  const isEnabled = enabled.includes(id);
34
36
  const inputId = `${id}-input`;
35
37
  const labelId = `${id}-label`;
@@ -44,8 +46,11 @@ export const PluginItem = ({
44
46
  [id, isEnabled, onChange],
45
47
  );
46
48
 
47
- const hasSettings = _hasSettings?.(id) ?? false;
49
+ const hasSettings = hasSettingsProp?.(id) ?? false;
48
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]';
49
54
 
50
55
  return (
51
56
  <ListItem.Root
@@ -53,67 +58,63 @@ export const PluginItem = ({
53
58
  labelId={labelId}
54
59
  data-testid={`pluginList.${id}`}
55
60
  aria-describedby={descriptionId}
56
- // TODO(burdon): Use Rail vars.
57
- classNames='is-full bs-full grid grid-cols-[48px_1fr_48px] grid-rows-[40px_1fr_32px] p-1 border border-separator rounded-md'
61
+ classNames={mx(gridCols, 'bs-[12rem] is-full gap-3 pie-2 border border-separator rounded-md overflow-hidden')}
58
62
  >
59
- {/* Header. */}
60
- <div className='flex flex-col grow justify-center items-center'>
61
- <Icon icon={icon} size={6} onClick={handleClick} classNames='text-subdued cursor-pointer' />
62
- </div>
63
- <div className='flex items-center overflow-hidden cursor-pointer' onClick={handleClick}>
64
- <span className='truncate'>{name ?? id}</span>
65
- </div>
66
- <div className='flex justify-center items-center'>
67
- <Input.Root id={inputId}>
68
- <Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
69
- </Input.Root>
63
+ <div className={mx(gridRows, 'justify-center rounded-l-md', styles.bg)}>
64
+ <div />
65
+ <Icon classNames={mx('cursor-pointer', styles.icon)} icon={icon} size={14} onClick={handleClick} />
70
66
  </div>
71
67
 
72
- {/* Body. */}
73
- <div />
74
- {(description || tags) && (
75
- <div id={descriptionId} className='col-span-2 flex flex-col w-full justify-between gap-2 pb-2'>
76
- <div className='grow'>
77
- <p className={mx(descriptionText, 'line-clamp-3 min-w-0 pie-2')}>{description}</p>
78
- </div>
79
- {tags && tags.length > 0 && (
80
- <div>
81
- {tags.map((tag) => (
82
- <Tag key={tag} palette={'indigo'} classNames='text-xs capitalize'>
83
- {tag}
84
- </Tag>
85
- ))}
86
- </div>
87
- )}
68
+ <div className={mx(gridRows)}>
69
+ <div className='flex items-center overflow-hidden cursor-pointer' onClick={handleClick}>
70
+ <span className='text-lg truncate'>{name ?? id}</span>
88
71
  </div>
89
- )}
90
72
 
91
- {/* Footer. */}
92
- <div />
93
- <div className='flex gap-2 items-center pie-1'>
94
- <Link
95
- aria-describedby={descriptionId}
96
- classNames='text-sm text-description cursor-pointer'
97
- onClick={handleClick}
98
- >
99
- {t('details label')}
100
- </Link>
73
+ <div>
74
+ <p className={mx(descriptionText, 'line-clamp-4 min-is-0')}>{description}</p>
75
+ </div>
101
76
 
102
- <div className='flex-1' />
103
- </div>
104
- <div className='flex justify-center items-center'>
105
- {hasSettings && (
77
+ <div className='flex -mis-0.5 overflow-x-auto scrollbar-none'>
78
+ {tags?.map((tag) => (
79
+ <Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase font-thin'>
80
+ {tag}
81
+ </Tag>
82
+ ))}
83
+ </div>
84
+
85
+ <div className='flex gap-2 items-center text-sm'>
106
86
  <IconButton
107
87
  aria-describedby={descriptionId}
108
- classNames='text-sm text-description cursor-pointer'
88
+ classNames='cursor-pointer'
109
89
  icon='ph--gear--regular'
110
90
  label={t('settings label')}
111
91
  iconOnly
112
92
  size={4}
113
93
  onClick={handleSettings}
94
+ disabled={!hasSettings}
114
95
  />
115
- )}
96
+
97
+ <Link aria-describedby={descriptionId} classNames='text-description cursor-pointer' onClick={handleClick}>
98
+ {t('details label')}
99
+ </Link>
100
+
101
+ <div className='grow' />
102
+ <div className='pie-1'>
103
+ <Input.Root id={inputId}>
104
+ <Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
105
+ </Input.Root>
106
+ </div>
107
+ </div>
116
108
  </div>
117
109
  </ListItem.Root>
118
110
  );
119
111
  };
112
+
113
+ const tagColors: Record<RegistryTagType, ChromaticPalette> = {
114
+ new: 'rose',
115
+ beta: 'teal',
116
+ labs: 'blue',
117
+ popular: 'green',
118
+ featured: 'pink',
119
+ experimental: 'amber',
120
+ };
@@ -2,19 +2,18 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type Meta } from '@storybook/react-vite';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import React, { useState } from 'react';
9
7
 
10
- import { definePlugin, type Plugin } from '@dxos/app-framework';
8
+ import { type Plugin, Plugin as PluginNS } from '@dxos/app-framework';
11
9
  import { faker } from '@dxos/random';
12
- import { type ThemedClassName } from '@dxos/react-ui';
13
- import { mx } from '@dxos/react-ui-theme';
14
- import { withTheme, withLayout } from '@dxos/storybook-utils';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { getHashHue } from '@dxos/ui-theme';
15
12
 
16
- import { PluginList } from './PluginList';
17
13
  import { translations } from '../translations';
14
+ import { RegistryTagType } from '../types';
15
+
16
+ import { PluginList } from './PluginList';
18
17
 
19
18
  faker.seed(1);
20
19
 
@@ -27,25 +26,21 @@ const icons = [
27
26
  'ph--github-logo--regular',
28
27
  ];
29
28
 
30
- const DefaultStory = ({ classNames }: ThemedClassName<{}>) => {
31
- const [plugins] = useState<Plugin[]>(
29
+ const DefaultStory = () => {
30
+ const [plugins] = useState<Plugin.Plugin[]>(
32
31
  faker.helpers.multiple(
33
32
  () =>
34
- definePlugin(
35
- {
36
- id: `dxos.org/plugin/plugin-${faker.string.uuid()}`,
37
- name: `${faker.commerce.productName()}`,
38
- description: faker.lorem.sentences(Math.ceil(Math.random() * 3)),
39
- tags: faker.datatype.boolean({ probability: 0.6 })
40
- ? [faker.helpers.arrayElement(['labs', 'beta', 'alpha', 'stable', 'new', '新発売'])]
41
- : undefined,
42
- icon: faker.helpers.arrayElement(icons),
43
- homePage: faker.datatype.boolean({ probability: 0.5 }) ? faker.internet.url() : undefined,
44
- source: faker.internet.url(),
45
- },
46
- [],
47
- ),
48
- { count: 16 },
33
+ PluginNS.define({
34
+ id: `dxos.org/plugin/plugin-${faker.string.uuid()}`,
35
+ name: `${faker.commerce.productName()}`,
36
+ description: faker.lorem.sentences(Math.ceil(Math.random() * 3)),
37
+ tags: faker.helpers.uniqueArray(RegistryTagType.literals as any, Math.floor(Math.random() * 3)),
38
+ icon: faker.helpers.arrayElement(icons),
39
+ iconHue: getHashHue(faker.string.uuid()),
40
+ homePage: faker.datatype.boolean({ probability: 0.5 }) ? faker.internet.url() : undefined,
41
+ source: faker.internet.url(),
42
+ }).pipe(PluginNS.make)(),
43
+ { count: 32 },
49
44
  ),
50
45
  );
51
46
  const [enabled, setEnabled] = useState<string[]>([]);
@@ -54,29 +49,29 @@ const DefaultStory = ({ classNames }: ThemedClassName<{}>) => {
54
49
  setEnabled((plugins) => (enabled ? [...plugins, id] : plugins.filter((plugin) => plugin === id)));
55
50
  };
56
51
 
57
- return (
58
- <div className={mx('flex overflow-hidden', classNames)}>
59
- <PluginList plugins={plugins} enabled={enabled} onChange={handleChange} hasSettings={() => true} />
60
- </div>
61
- );
52
+ return <PluginList plugins={plugins} enabled={enabled} onChange={handleChange} hasSettings={() => true} />;
62
53
  };
63
54
 
64
- const meta: Meta = {
55
+ const meta = {
65
56
  title: 'plugins/plugin-registry/PluginList',
66
57
  component: PluginList,
67
58
  render: DefaultStory,
68
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: 'justify-center' })],
69
59
  parameters: {
70
60
  translations,
71
61
  },
72
- };
62
+ } satisfies Meta<typeof PluginList>;
73
63
 
74
64
  export default meta;
75
65
 
76
- export const Default = {};
66
+ type Story = StoryObj<typeof meta>;
67
+
68
+ export const Default: Story = {
69
+ decorators: [withTheme, withLayout({ layout: 'column', scroll: true })],
70
+ };
77
71
 
78
- export const Column = {
79
- args: {
80
- classNames: 'w-[30rem]',
72
+ export const FullScreen: Story = {
73
+ decorators: [withTheme, withLayout({ scroll: true })],
74
+ parameters: {
75
+ layout: 'fullscreen',
81
76
  },
82
77
  };
@@ -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
  );
@@ -2,83 +2,82 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
5
7
  import React, { useCallback, useMemo } from 'react';
6
8
 
7
- import {
8
- Capabilities,
9
- createIntent,
10
- LayoutAction,
11
- type Plugin,
12
- SettingsAction,
13
- useCapability,
14
- useIntentDispatcher,
15
- usePluginManager,
16
- } from '@dxos/app-framework';
17
- import { ObservabilityAction } from '@dxos/plugin-observability/types';
18
- import { StackItem } from '@dxos/react-ui-stack';
9
+ import { Common, type Plugin, SettingsOperation } from '@dxos/app-framework';
10
+ import { useCapabilities, useOperationInvoker, usePluginManager } from '@dxos/app-framework/react';
11
+ import { runAndForwardErrors } from '@dxos/effect';
12
+ import { ObservabilityOperation } from '@dxos/plugin-observability/types';
13
+ import { Layout } from '@dxos/react-ui-mosaic';
19
14
 
20
15
  import { PluginList } from './PluginList';
21
16
 
22
- const sortByPluginMeta = ({ meta: { name: a = '' } }: Plugin, { meta: { name: b = '' } }: Plugin) => a.localeCompare(b);
17
+ const sortByPluginMeta = ({ meta: { name: a = '' } }: Plugin.Plugin, { meta: { name: b = '' } }: Plugin.Plugin) =>
18
+ a.localeCompare(b);
23
19
 
24
- export const RegistryContainer = ({ id, plugins: _plugins }: { id: string; plugins: Plugin[] }) => {
20
+ export type RegistryContainerProps = {
21
+ id: string;
22
+ plugins: Plugin.Plugin[];
23
+ };
24
+
25
+ export const RegistryContainer = ({ id, plugins: pluginsProp }: RegistryContainerProps) => {
25
26
  const manager = usePluginManager();
26
- const { dispatchPromise: dispatch } = useIntentDispatcher();
27
- const plugins = useMemo(() => _plugins.sort(sortByPluginMeta), [_plugins]);
28
- const settingsStore = useCapability(Capabilities.SettingsStore);
27
+ const { invoke, invokePromise } = useOperationInvoker();
28
+ const plugins = useMemo(() => pluginsProp.sort(sortByPluginMeta), [pluginsProp]);
29
+ const enabled = useAtomValue(manager.enabled);
30
+ const allSettings = useCapabilities(Common.Capability.Settings);
29
31
 
30
32
  // TODO(wittjosiah): Factor out to an intent?
31
33
  const handleChange = useCallback(
32
- async (id: string, enabled: boolean) => {
33
- if (enabled) {
34
- await manager.enable(id);
35
- } else {
36
- await manager.disable(id);
37
- }
34
+ (id: string, enabled: boolean) =>
35
+ Effect.gen(function* () {
36
+ if (enabled) {
37
+ yield* manager.enable(id);
38
+ } else {
39
+ yield* manager.disable(id);
40
+ }
38
41
 
39
- await dispatch(
40
- createIntent(ObservabilityAction.SendEvent, {
42
+ yield* invoke(ObservabilityOperation.SendEvent, {
41
43
  name: 'plugins.toggle',
42
44
  properties: {
43
45
  plugin: id,
44
46
  enabled,
45
47
  },
46
- }),
47
- );
48
- },
49
- [dispatch, manager],
48
+ });
49
+ }).pipe(runAndForwardErrors),
50
+ [invoke, manager],
50
51
  );
51
52
 
52
53
  const handleClick = useCallback(
53
54
  (pluginId: string) =>
54
- dispatch(
55
- createIntent(LayoutAction.Open, {
56
- part: 'main',
57
- // TODO(wittjosiah): `/` currently is not supported in ids.
58
- subject: [pluginId.replaceAll('/', ':')],
59
- options: { pivotId: id, positioning: 'end' },
60
- }),
61
- ),
62
- [dispatch, id],
55
+ invokePromise(Common.LayoutOperation.Open, {
56
+ // TODO(wittjosiah): `/` currently is not supported in ids.
57
+ subject: [pluginId.replaceAll('/', ':')],
58
+ pivotId: id,
59
+ positioning: 'end',
60
+ }),
61
+ [invokePromise, id],
63
62
  );
64
63
 
65
- const hasSettings = useCallback((pluginId: string) => !!settingsStore.getStore(pluginId), [settingsStore]);
64
+ const hasSettings = useCallback((pluginId: string) => allSettings.some((s) => s.prefix === pluginId), [allSettings]);
66
65
 
67
66
  const handleSettings = useCallback(
68
- (pluginId: string) => dispatch(createIntent(SettingsAction.Open, { plugin: pluginId })),
69
- [dispatch],
67
+ (pluginId: string) => invokePromise(SettingsOperation.Open, { plugin: pluginId }),
68
+ [invokePromise],
70
69
  );
71
70
 
72
71
  return (
73
- <StackItem.Content classNames='min-bs-0 overflow-y-auto scrollbar-thin contain-layout'>
72
+ <Layout.Container scrollable>
74
73
  <PluginList
75
74
  plugins={plugins}
76
- enabled={manager.enabled}
75
+ enabled={enabled}
77
76
  onClick={handleClick}
78
77
  onChange={handleChange}
79
78
  hasSettings={hasSettings}
80
79
  onSettings={handleSettings}
81
80
  />
82
- </StackItem.Content>
81
+ </Layout.Container>
83
82
  );
84
83
  };