@dxos/plugin-registry 0.9.0 → 0.9.1-staging.ee54ba693a

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 (145) hide show
  1. package/dist/lib/neutral/RegistryPlugin.node.mjs +709 -18
  2. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +4 -4
  3. package/dist/lib/neutral/chunk-ZY37XLVF.mjs +46 -0
  4. package/dist/lib/neutral/chunk-ZY37XLVF.mjs.map +7 -0
  5. package/dist/lib/neutral/index.mjs +1 -1
  6. package/dist/lib/neutral/meta.json +1 -1
  7. package/dist/lib/neutral/{open-plugin-registry-II735N76.mjs → open-plugin-registry-ID7LV3SF.mjs} +3 -3
  8. package/dist/lib/neutral/open-plugin-registry-ID7LV3SF.mjs.map +7 -0
  9. package/dist/lib/neutral/plugin.mjs +2 -2
  10. package/dist/lib/neutral/translations.mjs +6 -6
  11. package/dist/lib/neutral/translations.mjs.map +3 -3
  12. package/dist/types/dx.config.d.ts +28 -0
  13. package/dist/types/dx.config.d.ts.map +1 -0
  14. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -2
  15. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  16. package/dist/types/src/capabilities/index.d.ts +4 -12
  17. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  18. package/dist/types/src/capabilities/react-surface.d.ts +2 -2
  19. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  20. package/dist/types/src/capabilities/settings.d.ts +1 -10
  21. package/dist/types/src/capabilities/settings.d.ts.map +1 -1
  22. package/dist/types/src/categories.d.ts.map +1 -1
  23. package/dist/types/src/commands/index.d.ts +2 -2
  24. package/dist/types/src/commands/index.d.ts.map +1 -1
  25. package/dist/types/src/commands/plugin/disable.d.ts.map +1 -0
  26. package/dist/types/src/commands/plugin/enable.d.ts.map +1 -0
  27. package/dist/types/src/commands/plugin/index.d.ts +3 -0
  28. package/dist/types/src/commands/plugin/index.d.ts.map +1 -0
  29. package/dist/types/src/commands/plugin/list.d.ts.map +1 -0
  30. package/dist/types/src/commands/plugin/util.d.ts.map +1 -0
  31. package/dist/types/src/commands/registry/index.d.ts +16 -0
  32. package/dist/types/src/commands/registry/index.d.ts.map +1 -0
  33. package/dist/types/src/commands/registry/publish-package.d.ts +29 -0
  34. package/dist/types/src/commands/registry/publish-package.d.ts.map +1 -0
  35. package/dist/types/src/commands/registry/publish-publisher.d.ts +19 -0
  36. package/dist/types/src/commands/registry/publish-publisher.d.ts.map +1 -0
  37. package/dist/types/src/commands/registry/publish.d.ts +25 -0
  38. package/dist/types/src/commands/registry/publish.d.ts.map +1 -0
  39. package/dist/types/src/commands/registry/records.d.ts +14 -0
  40. package/dist/types/src/commands/registry/records.d.ts.map +1 -0
  41. package/dist/types/src/commands/registry/unpublish.d.ts +15 -0
  42. package/dist/types/src/commands/registry/unpublish.d.ts.map +1 -0
  43. package/dist/types/src/commands/registry/util.d.ts +113 -0
  44. package/dist/types/src/commands/registry/util.d.ts.map +1 -0
  45. package/dist/types/src/commands/registry/verify.d.ts +20 -0
  46. package/dist/types/src/commands/registry/verify.d.ts.map +1 -0
  47. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +2 -2
  48. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  49. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +6 -5
  50. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +1 -3
  52. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -1
  53. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  54. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
  55. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +5 -5
  56. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +5 -1
  58. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -1
  59. package/dist/types/src/components/RegistrySettings/RegistrySettings.stories.d.ts +78 -0
  60. package/dist/types/src/components/RegistrySettings/RegistrySettings.stories.d.ts.map +1 -0
  61. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -1
  62. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  63. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -1
  64. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +1 -1
  65. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -1
  66. package/dist/types/src/containers/RegistryArticle/index.d.ts +1 -1
  67. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -1
  68. package/dist/types/src/hooks/useAutoTags.d.ts +2 -2
  69. package/dist/types/src/hooks/useAutoTags.d.ts.map +1 -1
  70. package/dist/types/src/hooks/useCatalogEntry.d.ts +4 -5
  71. package/dist/types/src/hooks/useCatalogEntry.d.ts.map +1 -1
  72. package/dist/types/src/hooks/usePluginActions.d.ts +3 -3
  73. package/dist/types/src/hooks/usePluginActions.d.ts.map +1 -1
  74. package/dist/types/src/hooks/useUpdateAvailable.d.ts +2 -2
  75. package/dist/types/src/hooks/useUpdateAvailable.d.ts.map +1 -1
  76. package/dist/types/src/hooks/useVersionPicker.d.ts +7 -8
  77. package/dist/types/src/hooks/useVersionPicker.d.ts.map +1 -1
  78. package/dist/types/src/meta.d.ts +32 -2
  79. package/dist/types/src/meta.d.ts.map +1 -1
  80. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -1
  81. package/dist/types/src/translations.d.ts +5 -5
  82. package/dist/types/src/translations.d.ts.map +1 -1
  83. package/dist/types/src/types.d.ts.map +1 -1
  84. package/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/dx.config.ts +20 -0
  86. package/package.json +40 -33
  87. package/src/RegistryPlugin.node.ts +2 -2
  88. package/src/RegistryPlugin.test.ts +1 -1
  89. package/src/capabilities/app-graph-builder.ts +46 -41
  90. package/src/capabilities/dev-plugin-loader.ts +1 -1
  91. package/src/capabilities/react-surface.tsx +1 -4
  92. package/src/capabilities/settings.ts +2 -2
  93. package/src/categories.ts +5 -4
  94. package/src/commands/index.ts +2 -10
  95. package/src/commands/{disable.ts → plugin/disable.ts} +3 -3
  96. package/src/commands/{enable.ts → plugin/enable.ts} +3 -3
  97. package/src/commands/plugin/index.ts +14 -0
  98. package/src/commands/{list.ts → plugin/list.ts} +4 -4
  99. package/src/commands/registry/index.ts +30 -0
  100. package/src/commands/registry/publish-package.ts +119 -0
  101. package/src/commands/registry/publish-publisher.ts +73 -0
  102. package/src/commands/registry/publish.ts +278 -0
  103. package/src/commands/registry/records.ts +73 -0
  104. package/src/commands/registry/unpublish.ts +70 -0
  105. package/src/commands/registry/util.ts +395 -0
  106. package/src/commands/registry/verify.ts +76 -0
  107. package/src/components/DisableDependentsAlert/DisableDependentsAlert.tsx +1 -1
  108. package/src/components/PluginDetail/PluginDetail.stories.tsx +31 -15
  109. package/src/components/PluginDetail/PluginDetail.tsx +21 -23
  110. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +21 -24
  111. package/src/components/PluginList/PluginItem.tsx +16 -11
  112. package/src/components/PluginList/PluginList.stories.tsx +1 -2
  113. package/src/components/PluginList/PluginList.tsx +18 -13
  114. package/src/components/RegistrySettings/RegistrySettings.stories.tsx +45 -0
  115. package/src/components/RegistrySettings/RegistrySettings.tsx +44 -33
  116. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +2 -2
  117. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +3 -3
  118. package/src/containers/PluginArticle/PluginArticle.tsx +8 -6
  119. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +35 -28
  120. package/src/containers/PublicRegistryArticle/index.ts +1 -1
  121. package/src/containers/RegistryArticle/RegistryArticle.tsx +1 -1
  122. package/src/containers/RegistryArticle/index.ts +1 -1
  123. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +2 -2
  124. package/src/hooks/useAutoTags.ts +3 -3
  125. package/src/hooks/useCatalogEntry.ts +6 -7
  126. package/src/hooks/useDisableConfirmation.ts +1 -1
  127. package/src/hooks/usePluginActions.ts +13 -11
  128. package/src/hooks/useUpdateAvailable.ts +5 -5
  129. package/src/hooks/useVersionPicker.ts +22 -30
  130. package/src/meta.ts +6 -15
  131. package/src/operations/open-plugin-registry.ts +2 -2
  132. package/src/translations.ts +6 -6
  133. package/src/types.ts +3 -1
  134. package/dist/lib/neutral/chunk-GRQI6MLI.mjs +0 -36
  135. package/dist/lib/neutral/chunk-GRQI6MLI.mjs.map +0 -7
  136. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs.map +0 -7
  137. package/dist/types/src/commands/disable.d.ts.map +0 -1
  138. package/dist/types/src/commands/enable.d.ts.map +0 -1
  139. package/dist/types/src/commands/list.d.ts.map +0 -1
  140. package/dist/types/src/commands/util.d.ts.map +0 -1
  141. /package/dist/types/src/commands/{disable.d.ts → plugin/disable.d.ts} +0 -0
  142. /package/dist/types/src/commands/{enable.d.ts → plugin/enable.d.ts} +0 -0
  143. /package/dist/types/src/commands/{list.d.ts → plugin/list.d.ts} +0 -0
  144. /package/dist/types/src/commands/{util.d.ts → plugin/util.d.ts} +0 -0
  145. /package/src/commands/{util.ts → plugin/util.ts} +0 -0
@@ -15,8 +15,6 @@ type IconSize = 4 | 5 | 6 | 8;
15
15
 
16
16
  export type PluginFailureBadgeProps = {
17
17
  failure: PluginManager.PluginFailure;
18
- /** Optional override for the trigger icon's tailwind classes. */
19
- iconClassNames?: string;
20
18
  /** Visual size of the warning icon (passed through to react-ui `<Icon />`). */
21
19
  size?: IconSize;
22
20
  };
@@ -28,41 +26,40 @@ export type PluginFailureBadgeProps = {
28
26
  * message — enough for an operator to tell "remote host is offline" apart
29
27
  * from "the plugin crashed".
30
28
  */
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');
29
+ export const PluginFailureBadge = ({ failure, size }: PluginFailureBadgeProps) => {
30
+ const { t } = useTranslation(meta.profile.key);
37
31
 
38
32
  return (
39
33
  <Popover.Root>
40
34
  <Popover.Trigger asChild>
41
35
  <IconButton
42
- variant='ghost'
36
+ variant='destructive'
37
+ icon='ph--warning--bold'
43
38
  iconOnly
44
39
  noTooltip
45
- icon='ph--warning--bold'
46
40
  size={size}
47
- iconClassNames={iconClassNames}
48
41
  label={t('failure-badge.label')}
49
42
  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
43
  onClick={(event) => event.stopPropagation()}
53
44
  />
54
45
  </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>
46
+ <Popover.Portal>
47
+ <Popover.Content>
48
+ <Popover.Viewport>
49
+ <div className='px-3 py-2 min-w-[18rem] max-w-[28rem] flex flex-col gap-1'>
50
+ <p className='font-medium text-sm'>
51
+ {t('failure-title.label', {
52
+ phase: failure.phase === 'load' ? t('failure-phase-load.label') : t('failure-phase-activation.label'),
53
+ reason:
54
+ failure.reason === 'timeout' ? t('failure-reason-timeout.label') : t('failure-reason-error.label'),
55
+ })}
56
+ </p>
57
+ <p className='text-description text-sm break-words'>{failure.error.message}</p>
58
+ </div>
59
+ </Popover.Viewport>
60
+ <Popover.Arrow />
61
+ </Popover.Content>
62
+ </Popover.Portal>
66
63
  </Popover.Root>
67
64
  );
68
65
  };
@@ -12,11 +12,11 @@ import {
12
12
  IconButton,
13
13
  Input,
14
14
  Link,
15
- ListItem,
16
15
  type NeutralPalette,
17
16
  Tag,
18
17
  useTranslation,
19
18
  } from '@dxos/react-ui';
19
+ import { Listbox } from '@dxos/react-ui-list';
20
20
  import { mx } from '@dxos/ui-theme';
21
21
  import { getStyles } from '@dxos/ui-theme';
22
22
 
@@ -78,8 +78,10 @@ export const PluginItem = ({
78
78
  onSettings,
79
79
  failure,
80
80
  }: PluginItemProps) => {
81
- const { t } = useTranslation(meta.id);
82
- const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
81
+ const { t } = useTranslation(meta.profile.key);
82
+ const { key: id, name, description, tags, icon: rawIcon } = plugin.meta.profile;
83
+ const icon = rawIcon?.key ?? 'ph--circle--regular';
84
+ const iconHue = rawIcon?.hue ?? 'neutral';
83
85
  const displayTags = useMemo(() => {
84
86
  if (!extraTags || extraTags.length === 0) {
85
87
  return tags ?? [];
@@ -97,7 +99,6 @@ export const PluginItem = ({
97
99
  const showInstallButton = !!onInstall && !isInstalled;
98
100
  const showUpdateButton = !!onUpdate && isInstalled && !!hasUpdate;
99
101
  const inputId = `${id}-input`;
100
- const labelId = `${id}-label`;
101
102
  const descriptionId = `${id}-description`;
102
103
  const handleClick = useCallback(() => onClick?.(id), [id, onClick]);
103
104
 
@@ -132,12 +133,16 @@ export const PluginItem = ({
132
133
  const gridRows = 'grid grid-cols-1 grid-rows-[40px_1fr_min-content_40px]';
133
134
 
134
135
  return (
135
- <ListItem.Root
136
- key={id}
137
- labelId={labelId}
136
+ <Listbox.Item
137
+ id={id}
138
138
  data-testid={`pluginList.${id}`}
139
139
  aria-describedby={descriptionId}
140
- classNames={mx(gridCols, 'h-[14rem] w-full gap-3 pe-2 bg-modal-surface rounded-md overflow-hidden')}
140
+ classNames={mx(
141
+ gridCols,
142
+ // Override `Listbox.Item`'s default row chrome (flex/items-center/padding/cursor) so the
143
+ // bespoke card grid stretches both columns to full height and controls its own padding.
144
+ 'items-stretch p-0 pe-2 cursor-default h-[14rem] w-full gap-3 bg-modal-surface rounded-md overflow-hidden',
145
+ )}
141
146
  >
142
147
  <div className={mx(gridRows, 'rounded-l-md', styles.surface)}>
143
148
  <div className='flex justify-center row-start-2 cursor-pointer' onClick={handleClick}>
@@ -156,8 +161,8 @@ export const PluginItem = ({
156
161
  </div>
157
162
 
158
163
  <div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
159
- {displayTags.map((tag) => (
160
- <Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase'>
164
+ {displayTags.map((tag: string) => (
165
+ <Tag key={tag} hue={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase'>
161
166
  {tag}
162
167
  </Tag>
163
168
  ))}
@@ -207,7 +212,7 @@ export const PluginItem = ({
207
212
  </div>
208
213
  </div>
209
214
  </div>
210
- </ListItem.Root>
215
+ </Listbox.Item>
211
216
  );
212
217
  };
213
218
 
@@ -38,8 +38,7 @@ const DefaultStory = () => {
38
38
  name: `${random.commerce.productName()}`,
39
39
  description: random.lorem.sentences(Math.ceil(Math.random() * 3)),
40
40
  tags: random.helpers.uniqueArray(RegistryTagType.literals as any, Math.floor(Math.random() * 3)),
41
- icon: random.helpers.arrayElement(icons),
42
- iconHue: getHashHue(random.string.uuid()),
41
+ icon: { key: random.helpers.arrayElement(icons), hue: getHashHue(random.string.uuid()) },
43
42
  homePage: random.datatype.boolean({ probability: 0.5 }) ? random.internet.url() : undefined,
44
43
  source: random.internet.url(),
45
44
  }),
@@ -5,7 +5,7 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { type Plugin, type PluginManager } from '@dxos/app-framework';
8
- import { List } from '@dxos/react-ui';
8
+ import { Listbox } from '@dxos/react-ui-list';
9
9
 
10
10
  import { PluginItem, type PluginItemProps } from './PluginItem';
11
11
 
@@ -36,17 +36,22 @@ export const PluginList = ({
36
36
  ...props
37
37
  }: PluginListProps) => {
38
38
  return (
39
- <List classNames='grid grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] auto-rows-[max-content] gap-4 p-4'>
40
- {plugins.map((plugin) => (
41
- <PluginItem
42
- key={plugin.meta.id}
43
- plugin={plugin}
44
- extraTags={extraTagsById?.[plugin.meta.id]}
45
- hasUpdate={updateAvailableIds?.has(plugin.meta.id)}
46
- failure={failuresById?.[plugin.meta.id]}
47
- {...props}
48
- />
49
- ))}
50
- </List>
39
+ <Listbox.Root>
40
+ <Listbox.Content
41
+ aria-label='plugins'
42
+ classNames='grid grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] auto-rows-[max-content] gap-4 p-4'
43
+ >
44
+ {plugins.map((plugin) => (
45
+ <PluginItem
46
+ key={plugin.meta.profile.key}
47
+ plugin={plugin}
48
+ extraTags={extraTagsById?.[plugin.meta.profile.key]}
49
+ hasUpdate={updateAvailableIds?.has(plugin.meta.profile.key)}
50
+ failure={failuresById?.[plugin.meta.profile.key]}
51
+ {...props}
52
+ />
53
+ ))}
54
+ </Listbox.Content>
55
+ </Listbox.Root>
51
56
  );
52
57
  };
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+
7
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
8
+
9
+ import { translations } from '#translations';
10
+
11
+ import { RegistrySettings } from './RegistrySettings';
12
+
13
+ const meta = {
14
+ title: 'plugins/plugin-registry/components/RegistrySettings',
15
+ component: RegistrySettings,
16
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
17
+ parameters: {
18
+ layout: 'fullscreen',
19
+ translations,
20
+ },
21
+ } satisfies Meta<typeof RegistrySettings>;
22
+
23
+ export default meta;
24
+
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ export const Default: Story = {
28
+ args: {
29
+ settings: { devPluginUrl: 'http://localhost:3967', devPluginEnabled: false },
30
+ onSettingsChange: () => {},
31
+ activeDevPluginIds: [],
32
+ onEnableDev: async () => {},
33
+ onDisableDev: async () => {},
34
+ },
35
+ };
36
+
37
+ export const Enabled: Story = {
38
+ args: {
39
+ settings: { devPluginUrl: 'http://localhost:3967', devPluginEnabled: true },
40
+ onSettingsChange: () => {},
41
+ activeDevPluginIds: [],
42
+ onEnableDev: async () => {},
43
+ onDisableDev: async () => {},
44
+ },
45
+ };
@@ -7,13 +7,13 @@ import React, { useCallback, useState } from 'react';
7
7
  import { type AppSurface } from '@dxos/app-toolkit/ui';
8
8
  import { log } from '@dxos/log';
9
9
  import { Button, Input, Message, useTranslation } from '@dxos/react-ui';
10
- import { Settings as SettingsForm } from '@dxos/react-ui-form';
10
+ import { Form } from '@dxos/react-ui-form';
11
11
 
12
12
  import { meta } from '#meta';
13
13
 
14
- import { type RegistrySettings as RegistrySettingsType } from '../../types';
14
+ import { RegistrySettingsSchema, type RegistrySettings as RegistrySettingsType } from '../../types';
15
15
 
16
- export type RegistrySettingsProps = AppSurface.SettingsArticleProps<
16
+ export type RegistrySettingsProps = AppSurface.SettingsProps<
17
17
  RegistrySettingsType,
18
18
  {
19
19
  activeDevPluginIds: readonly string[];
@@ -29,6 +29,10 @@ export type RegistrySettingsProps = AppSurface.SettingsArticleProps<
29
29
  * re-attach it on the next reload, surviving HMR-triggered reloads. If the
30
30
  * dev server is offline at boot, the toggle stays on and a warning is logged
31
31
  * (the manager's `failed` atom also surfaces a badge on the plugin list).
32
+ *
33
+ * The URL input and toggle are rendered as `Form.Row` action rows (not schema
34
+ * fields): the input needs a dynamic disabled state and the toggle runs async
35
+ * enable/disable side effects, neither of which a plain schema field expresses.
32
36
  */
33
37
  export const RegistrySettings = ({
34
38
  settings,
@@ -37,7 +41,7 @@ export const RegistrySettings = ({
37
41
  onEnableDev,
38
42
  onDisableDev,
39
43
  }: RegistrySettingsProps) => {
40
- const { t } = useTranslation(meta.id);
44
+ const { t } = useTranslation(meta.profile.key);
41
45
  const [busy, setBusy] = useState(false);
42
46
  const enabled = !!settings.devPluginEnabled;
43
47
  const url = settings.devPluginUrl ?? '';
@@ -87,34 +91,41 @@ export const RegistrySettings = ({
87
91
  : t('dev-plugin.enable.label');
88
92
 
89
93
  return (
90
- <SettingsForm.Viewport>
91
- <SettingsForm.Section title={t('dev-plugin.section.title')}>
92
- <Message.Root valence='neutral'>
93
- <Message.Content>{t('dev-plugin.description')}</Message.Content>
94
- </Message.Root>
95
- <SettingsForm.Item title={t('dev-plugin.url.label')} description={t('dev-plugin.url.description')}>
96
- <Input.TextInput
97
- classNames='ms-2'
98
- disabled={!onSettingsChange || enabled || busy}
99
- value={url}
100
- onChange={(event) => onSettingsChange?.((current) => ({ ...current, devPluginUrl: event.target.value }))}
101
- />
102
- </SettingsForm.Item>
103
- <SettingsForm.Item title={t('dev-plugin.toggle.label')} description={t('dev-plugin.toggle.description')}>
104
- <Button
105
- variant={enabled ? undefined : 'primary'}
106
- disabled={busy || (!enabled && !trimmedUrl)}
107
- onClick={() => void handleToggle()}
108
- >
109
- {buttonLabel}
110
- </Button>
111
- </SettingsForm.Item>
112
- {enabled && !loadedDevId && !busy && (
113
- <Message.Root valence='warning'>
114
- <Message.Content>{t('dev-plugin.not-loaded.message')}</Message.Content>
115
- </Message.Root>
116
- )}
117
- </SettingsForm.Section>
118
- </SettingsForm.Viewport>
94
+ <Form.Root variant='settings' readonly={!onSettingsChange} schema={RegistrySettingsSchema} values={settings}>
95
+ <Form.Viewport scroll>
96
+ <Form.Content>
97
+ <Form.Section title={t('dev-plugin.section.title')}>
98
+ <Message.Root valence='neutral'>
99
+ <Message.Content>{t('dev-plugin.description')}</Message.Content>
100
+ </Message.Root>
101
+ <Form.Row label={t('dev-plugin.url.label')} description={t('dev-plugin.url.description')}>
102
+ <Input.Root>
103
+ <Input.TextInput
104
+ disabled={!onSettingsChange || enabled || busy}
105
+ value={url}
106
+ onChange={(event) =>
107
+ onSettingsChange?.((current) => ({ ...current, devPluginUrl: event.target.value }))
108
+ }
109
+ />
110
+ </Input.Root>
111
+ </Form.Row>
112
+ <Form.Row label={t('dev-plugin.toggle.label')} description={t('dev-plugin.toggle.description')}>
113
+ <Button
114
+ variant={enabled ? undefined : 'primary'}
115
+ disabled={!onSettingsChange || busy || (!enabled && !trimmedUrl)}
116
+ onClick={() => void handleToggle()}
117
+ >
118
+ {buttonLabel}
119
+ </Button>
120
+ </Form.Row>
121
+ {enabled && !loadedDevId && !busy && (
122
+ <Message.Root valence='warning'>
123
+ <Message.Content>{t('dev-plugin.not-loaded.message')}</Message.Content>
124
+ </Message.Root>
125
+ )}
126
+ </Form.Section>
127
+ </Form.Content>
128
+ </Form.Viewport>
129
+ </Form.Root>
119
130
  );
120
131
  };
@@ -20,7 +20,7 @@ import { getPluginPath, meta } from '#meta';
20
20
  import { useDisableConfirmation } from '../../hooks';
21
21
 
22
22
  const matchesFilter = (plugin: Plugin.Plugin, query: string) => {
23
- const haystack = `${plugin.meta.name ?? ''} ${plugin.meta.id}`.toLowerCase();
23
+ const haystack = `${plugin.meta.profile.name ?? ''} ${plugin.meta.profile.key}`.toLowerCase();
24
24
  return haystack.includes(query);
25
25
  };
26
26
 
@@ -67,7 +67,7 @@ export const BaseRegistryArticle = composable<HTMLDivElement, BaseRegistryArticl
67
67
  },
68
68
  forwardedRef,
69
69
  ) => {
70
- const { t } = useTranslation(meta.id);
70
+ const { t } = useTranslation(meta.profile.key);
71
71
  const manager = usePluginManager();
72
72
  const { invoke, invokePromise } = useOperationInvoker();
73
73
  const allSettings = useCapabilities(AppCapabilities.Settings);
@@ -12,11 +12,11 @@ import { Button, Dialog, Input, useTranslation } from '@dxos/react-ui';
12
12
 
13
13
  import { meta } from '../../meta';
14
14
 
15
- export const LOAD_PLUGIN_DIALOG = DXN.make(`${meta.id}.loadPluginDialog`);
15
+ export const LOAD_PLUGIN_DIALOG = DXN.make(`${meta.profile.key}.loadPluginDialog`);
16
16
 
17
17
  export const LoadPluginDialog = () => {
18
18
  const manager = usePluginManager();
19
- const { t } = useTranslation(meta.id);
19
+ const { t } = useTranslation(meta.profile.key);
20
20
  const [url, setUrl] = useState('');
21
21
  const [loading, setLoading] = useState(false);
22
22
  const [error, setError] = useState<string | null>(null);
@@ -33,7 +33,7 @@ export const LoadPluginDialog = () => {
33
33
 
34
34
  void Effect.gen(function* () {
35
35
  const plugin = yield* manager.add(trimmed);
36
- yield* manager.enable(plugin.meta.id);
36
+ yield* manager.enable(plugin.meta.profile.key);
37
37
  closeRef.current?.click();
38
38
  }).pipe(
39
39
  Effect.catchAll((err) =>
@@ -24,19 +24,18 @@ import {
24
24
  export type PluginArticleProps = { subject: Plugin.Plugin };
25
25
 
26
26
  export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
27
- const pluginId = plugin.meta.id;
27
+ const pluginId = plugin.meta.profile.key;
28
28
  const manager = usePluginManager();
29
29
  const plugins = useAtomValue(manager.plugins);
30
30
  const remotePluginIds = useRemotePluginIds();
31
31
  const provider = useRegistryPluginProvider();
32
32
  const { invokePromise } = useOperationInvoker();
33
33
 
34
- const { catalogEntry, moduleUrl, repo } = useCatalogEntry(pluginId);
34
+ const { catalogEntry, moduleUrl } = useCatalogEntry(pluginId);
35
35
  const { installedVersionTag, syncInstalledVersion } = useInstalledVersionTag(pluginId, plugins);
36
36
  const { pickerVersions, selectedVersionTag, setSelectedVersionTag } = useVersionPicker({
37
37
  provider,
38
38
  pluginId,
39
- repo,
40
39
  moduleUrl,
41
40
  installedVersionTag,
42
41
  });
@@ -44,11 +43,14 @@ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
44
43
  const enabled = manager.getEnabled().includes(pluginId);
45
44
  const failed = useAtomValue(manager.failed);
46
45
  const failure = useMemo(() => failed.find((entry) => entry.id === pluginId), [failed, pluginId]);
47
- const isInstalled = useMemo(() => plugins.some((candidate) => candidate.meta.id === pluginId), [plugins, pluginId]);
46
+ const isInstalled = useMemo(
47
+ () => plugins.some((candidate) => candidate.meta.profile.key === pluginId),
48
+ [plugins, pluginId],
49
+ );
48
50
  const isCore = manager.getCore().includes(pluginId);
49
51
  const canUninstall = isInstalled && !isCore && remotePluginIds.has(pluginId);
50
52
  const hasUpdate =
51
- isInstalled && !!catalogEntry && !!installedVersionTag && installedVersionTag !== catalogEntry.version;
53
+ isInstalled && !!catalogEntry && !!installedVersionTag && installedVersionTag !== catalogEntry.release?.version;
52
54
 
53
55
  // Recompute graph slices whenever the plugin list changes, so installs /
54
56
  // removals through other surfaces (or this article's own actions) keep the
@@ -66,7 +68,7 @@ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
66
68
  // only loaded when it is activated, so the always-available `meta.name`
67
69
  // from the registered plugin set is the right source for chip labels.
68
70
  const handleResolvePluginName = useCallback(
69
- (id: string): string => plugins.find((candidate) => candidate.meta.id === id)?.meta.name ?? id,
71
+ (id: string): string => plugins.find((candidate) => candidate.meta.profile.key === id)?.meta.profile.name ?? id,
70
72
  [plugins],
71
73
  );
72
74
 
@@ -6,7 +6,7 @@ import { useAtomValue } from '@effect-atom/atom-react';
6
6
  import * as Effect from 'effect/Effect';
7
7
  import React, { useCallback, useMemo, useState } from 'react';
8
8
 
9
- import { type Registry, Plugin, UrlLoader } from '@dxos/app-framework';
9
+ import { Plugin, UrlLoader } from '@dxos/app-framework';
10
10
  import { useOperationInvoker, usePluginManager } from '@dxos/app-framework/ui';
11
11
  import { EffectEx } from '@dxos/effect';
12
12
  import { DXN } from '@dxos/keys';
@@ -19,29 +19,30 @@ import { meta } from '#meta';
19
19
  import { useAutoTags, useRegistryPlugins, useUpdateAvailableIds } from '../../hooks';
20
20
  import { BaseRegistryArticle } from '../BaseRegistryArticle';
21
21
 
22
- const sortEntries = (a: Registry.Plugin, b: Registry.Plugin) => (a.name ?? a.id).localeCompare(b.name ?? b.id);
22
+ const sortEntries = (a: Plugin.Meta, b: Plugin.Meta) =>
23
+ (a.profile.name ?? a.profile.key).localeCompare(b.profile.name ?? b.profile.key);
23
24
 
24
25
  const sortPlugins = (a: Plugin.Plugin, b: Plugin.Plugin) =>
25
- (a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id);
26
+ (a.meta.profile.name ?? a.meta.profile.key).localeCompare(b.meta.profile.name ?? b.meta.profile.key);
26
27
 
27
28
  /**
28
29
  * Turns a registry catalog entry into a minimal Plugin object so we can reuse
29
30
  * {@link BaseRegistryArticle} for rendering. The synthesized plugin has no
30
31
  * modules — it exists only for display until the user installs it.
31
32
  */
32
- const toDisplayPlugin = (plugin: Registry.Plugin): Plugin.Plugin =>
33
+ const toDisplayPlugin = (plugin: Plugin.Meta): Plugin.Plugin =>
33
34
  ({
34
35
  [Plugin.PluginTypeId]: Plugin.PluginTypeId,
35
36
  meta: Plugin.makeMeta({
36
- key: DXN.make(plugin.id),
37
- name: plugin.name,
38
- description: plugin.description,
39
- homePage: plugin.homePage,
40
- source: plugin.source,
41
- screenshots: plugin.screenshots,
42
- tags: plugin.tags,
43
- icon: plugin.icon,
44
- iconHue: plugin.iconHue,
37
+ key: DXN.make(plugin.profile.key),
38
+ name: plugin.profile.name,
39
+ description: plugin.profile.description,
40
+ homePage: plugin.profile.homePage,
41
+ source: plugin.profile.source,
42
+ screenshots: plugin.profile.screenshots,
43
+ tags: plugin.profile.tags,
44
+ icon: plugin.profile.icon,
45
+ author: plugin.profile.author,
45
46
  }),
46
47
  modules: [],
47
48
  }) as Plugin.Plugin;
@@ -52,18 +53,18 @@ export type PublicRegistryArticleProps = {
52
53
 
53
54
  export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryArticleProps>(
54
55
  ({ id, ...props }, forwardedRef) => {
55
- const { t } = useTranslation(meta.id);
56
+ const { t } = useTranslation(meta.profile.key);
56
57
  const manager = usePluginManager();
57
58
  const { invoke } = useOperationInvoker();
58
59
  const { entries, loading, error } = useRegistryPlugins();
59
60
  const plugins = useAtomValue(manager.plugins);
60
- const installedIds = useMemo(() => plugins.map((plugin) => plugin.meta.id), [plugins]);
61
+ const installedIds = useMemo(() => plugins.map((plugin) => plugin.meta.profile.key), [plugins]);
61
62
  const extraTagsById = useAutoTags(entries);
62
63
 
63
64
  // Snapshot of installed plugin ids at mount time. Used to sort installed
64
65
  // plugins to the top without having newly-installed rows jump up mid-session.
65
66
  const [installedSnapshot] = useState<ReadonlySet<string>>(
66
- () => new Set(manager.getPlugins().map((plugin) => plugin.meta.id)),
67
+ () => new Set(manager.getPlugins().map((plugin) => plugin.meta.profile.key)),
67
68
  );
68
69
 
69
70
  const [installingIds, setInstallingIds] = useState<readonly string[]>([]);
@@ -74,7 +75,9 @@ export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryAr
74
75
  const moduleUrlById = useMemo(() => {
75
76
  const map: Record<string, string> = {};
76
77
  for (const entry of sortedEntries) {
77
- map[entry.id] = entry.moduleUrl;
78
+ if (entry.release?.moduleUrl) {
79
+ map[entry.profile.key] = entry.release.moduleUrl;
80
+ }
78
81
  }
79
82
  return map;
80
83
  }, [sortedEntries]);
@@ -83,7 +86,9 @@ export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryAr
83
86
  const versionById = useMemo(() => {
84
87
  const map: Record<string, string> = {};
85
88
  for (const entry of sortedEntries) {
86
- map[entry.id] = entry.version;
89
+ if (entry.release?.version) {
90
+ map[entry.profile.key] = entry.release.version;
91
+ }
87
92
  }
88
93
  return map;
89
94
  }, [sortedEntries]);
@@ -92,13 +97,15 @@ export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryAr
92
97
  // is based on the mount-time snapshot so rows don't jump as the user
93
98
  // installs plugins during the session.
94
99
  const items = useMemo(() => {
95
- const catalogIds = new Set(sortedEntries.map((entry) => entry.id));
100
+ const catalogIds = new Set(sortedEntries.map((entry) => entry.profile.key));
96
101
  const remoteIds = new Set(UrlLoader.getRemoteEntries().map((entry) => entry.id));
97
102
  const fromCatalog = sortedEntries.map(toDisplayPlugin);
98
- const fromUrlOnly = plugins.filter((plugin) => remoteIds.has(plugin.meta.id) && !catalogIds.has(plugin.meta.id));
103
+ const fromUrlOnly = plugins.filter(
104
+ (plugin) => remoteIds.has(plugin.meta.profile.key) && !catalogIds.has(plugin.meta.profile.key),
105
+ );
99
106
  const installedFirst = [...fromCatalog, ...fromUrlOnly].sort((a, b) => {
100
- const aInstalled = installedSnapshot.has(a.meta.id);
101
- const bInstalled = installedSnapshot.has(b.meta.id);
107
+ const aInstalled = installedSnapshot.has(a.meta.profile.key);
108
+ const bInstalled = installedSnapshot.has(b.meta.profile.key);
102
109
  if (aInstalled !== bInstalled) {
103
110
  return aInstalled ? -1 : 1;
104
111
  }
@@ -117,14 +124,14 @@ export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryAr
117
124
  setInstallingIds((prev) => (prev.includes(pluginId) ? prev : [...prev, pluginId]));
118
125
  void Effect.gen(function* () {
119
126
  const plugin = yield* manager.add(moduleUrl);
120
- yield* manager.enable(plugin.meta.id);
127
+ yield* manager.enable(plugin.meta.profile.key);
121
128
  // Persist the installed version for future update detection.
122
129
  if (version) {
123
- UrlLoader.setInstalledVersion(plugin.meta.id, version);
130
+ UrlLoader.setInstalledVersion(plugin.meta.profile.key, version);
124
131
  }
125
132
  yield* invoke(ObservabilityOperation.SendEvent, {
126
133
  name: 'plugins.install',
127
- properties: { plugin: plugin.meta.id, source: 'registry' },
134
+ properties: { plugin: plugin.meta.profile.key, source: 'registry' },
128
135
  });
129
136
  }).pipe(
130
137
  Effect.ensuring(Effect.sync(() => setInstallingIds((prev) => prev.filter((pid) => pid !== pluginId)))),
@@ -146,13 +153,13 @@ export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryAr
146
153
  // Unload the old version then re-install from the new URL.
147
154
  yield* manager.remove(pluginId);
148
155
  const plugin = yield* manager.add(moduleUrl);
149
- yield* manager.enable(plugin.meta.id);
156
+ yield* manager.enable(plugin.meta.profile.key);
150
157
  if (version) {
151
- UrlLoader.setInstalledVersion(plugin.meta.id, version);
158
+ UrlLoader.setInstalledVersion(plugin.meta.profile.key, version);
152
159
  }
153
160
  yield* invoke(ObservabilityOperation.SendEvent, {
154
161
  name: 'plugins.update',
155
- properties: { plugin: plugin.meta.id, source: 'registry' },
162
+ properties: { plugin: plugin.meta.profile.key, source: 'registry' },
156
163
  });
157
164
  }).pipe(
158
165
  Effect.ensuring(Effect.sync(() => setUpdatingIds((prev) => prev.filter((pid) => pid !== pluginId)))),
@@ -2,4 +2,4 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- export { PublicRegistryArticle as default, PublicRegistryArticle } from './PublicRegistryArticle';
5
+ export { PublicRegistryArticle, PublicRegistryArticle as default } from './PublicRegistryArticle';
@@ -12,7 +12,7 @@ import { composable } from '@dxos/react-ui';
12
12
  import { BaseRegistryArticle } from '../BaseRegistryArticle';
13
13
 
14
14
  const sortByPluginMeta = (a: Plugin.Plugin, b: Plugin.Plugin) =>
15
- (a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id);
15
+ (a.meta.profile.name ?? a.meta.profile.key).localeCompare(b.meta.profile.name ?? b.meta.profile.key);
16
16
 
17
17
  export type RegistryArticleProps = {
18
18
  id: string;
@@ -2,4 +2,4 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { RegistryArticle as default, RegistryArticle } from './RegistryArticle';
5
+ export { RegistryArticle, RegistryArticle as default } from './RegistryArticle';
@@ -6,7 +6,7 @@ import { useAtomValue } from '@effect-atom/atom-react';
6
6
  import * as Effect from 'effect/Effect';
7
7
  import React, { useCallback } from 'react';
8
8
 
9
- import { useSettingsState, usePluginManager } from '@dxos/app-framework/ui';
9
+ import { usePluginManager, useSettingsState } from '@dxos/app-framework/ui';
10
10
  import { type AppCapabilities } from '@dxos/app-toolkit';
11
11
  import { EffectEx } from '@dxos/effect';
12
12
 
@@ -33,7 +33,7 @@ export const RegistrySettingsContainer = ({ subject }: RegistrySettingsContainer
33
33
  await EffectEx.runAndForwardErrors(
34
34
  Effect.gen(function* () {
35
35
  const plugin = yield* manager.add(url);
36
- yield* manager.enable(plugin.meta.id);
36
+ yield* manager.enable(plugin.meta.profile.key);
37
37
  }),
38
38
  );
39
39
  },