@dxos/plugin-registry 0.9.1-main.c7dcc2e112 → 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 (50) hide show
  1. package/dist/lib/neutral/RegistryPlugin.node.mjs +10 -10
  2. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +3 -3
  3. package/dist/lib/neutral/meta.json +1 -1
  4. package/dist/lib/neutral/translations.mjs +5 -5
  5. package/dist/lib/neutral/translations.mjs.map +1 -1
  6. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -2
  7. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  8. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  9. package/dist/types/src/capabilities/settings.d.ts +1 -10
  10. package/dist/types/src/capabilities/settings.d.ts.map +1 -1
  11. package/dist/types/src/commands/registry/publish-package.d.ts +1 -1
  12. package/dist/types/src/commands/registry/publish.d.ts +2 -2
  13. package/dist/types/src/commands/registry/unpublish.d.ts +2 -2
  14. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +6 -5
  15. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +1 -3
  17. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -1
  18. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  19. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
  20. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +5 -5
  21. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +5 -1
  22. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -1
  23. package/dist/types/src/components/RegistrySettings/RegistrySettings.stories.d.ts +78 -0
  24. package/dist/types/src/components/RegistrySettings/RegistrySettings.stories.d.ts.map +1 -0
  25. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +1 -1
  26. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -1
  27. package/dist/types/src/containers/RegistryArticle/index.d.ts +1 -1
  28. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -1
  29. package/dist/types/src/meta.d.ts +4 -0
  30. package/dist/types/src/meta.d.ts.map +1 -1
  31. package/dist/types/src/translations.d.ts +5 -5
  32. package/dist/types/src/translations.d.ts.map +1 -1
  33. package/dist/types/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +39 -39
  35. package/src/capabilities/app-graph-builder.ts +3 -2
  36. package/src/commands/registry/publish-package.ts +5 -5
  37. package/src/commands/registry/publish.ts +5 -5
  38. package/src/commands/registry/records.ts +1 -1
  39. package/src/commands/registry/unpublish.ts +6 -6
  40. package/src/commands/registry/util.ts +1 -1
  41. package/src/components/PluginDetail/PluginDetail.stories.tsx +31 -17
  42. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +20 -23
  43. package/src/components/PluginList/PluginItem.tsx +11 -8
  44. package/src/components/PluginList/PluginList.tsx +18 -13
  45. package/src/components/RegistrySettings/RegistrySettings.stories.tsx +45 -0
  46. package/src/components/RegistrySettings/RegistrySettings.tsx +43 -32
  47. package/src/containers/PublicRegistryArticle/index.ts +1 -1
  48. package/src/containers/RegistryArticle/index.ts +1 -1
  49. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +1 -1
  50. package/src/translations.ts +5 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-registry",
3
- "version": "0.9.1-main.c7dcc2e112",
3
+ "version": "0.9.1-staging.ee54ba693a",
4
4
  "description": "Braneframe plugin gallery",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -59,52 +59,52 @@
59
59
  "@effect-atom/atom-react": "^0.5.0",
60
60
  "@effect/cli": "0.75.2",
61
61
  "yaml": "^2.8.2",
62
- "@dxos/app-framework": "0.9.1-main.c7dcc2e112",
63
- "@dxos/app-toolkit": "0.9.1-main.c7dcc2e112",
64
- "@dxos/client": "0.9.1-main.c7dcc2e112",
65
- "@dxos/client-protocol": "0.9.1-main.c7dcc2e112",
66
- "@dxos/cli-util": "0.9.1-main.c7dcc2e112",
67
- "@dxos/compute": "0.9.1-main.c7dcc2e112",
68
- "@dxos/edge-client": "0.9.1-main.c7dcc2e112",
69
- "@dxos/invariant": "0.9.1-main.c7dcc2e112",
70
- "@dxos/echo": "0.9.1-main.c7dcc2e112",
71
- "@dxos/keys": "0.9.1-main.c7dcc2e112",
72
- "@dxos/plugin-graph": "0.9.1-main.c7dcc2e112",
73
- "@dxos/log": "0.9.1-main.c7dcc2e112",
74
- "@dxos/protocols": "0.9.1-main.c7dcc2e112",
75
- "@dxos/context": "0.9.1-main.c7dcc2e112",
76
- "@dxos/plugin-observability": "0.9.1-main.c7dcc2e112",
77
- "@dxos/react-ui-form": "0.9.1-main.c7dcc2e112",
78
- "@dxos/react-ui-mosaic": "0.9.1-main.c7dcc2e112",
79
- "@dxos/react-ui-stack": "0.9.1-main.c7dcc2e112",
80
- "@dxos/types": "0.9.1-main.c7dcc2e112",
81
- "@dxos/react-ui-markdown": "0.9.1-main.c7dcc2e112",
82
- "@dxos/util": "0.9.1-main.c7dcc2e112",
83
- "@dxos/effect": "0.9.1-main.c7dcc2e112"
62
+ "@dxos/app-framework": "0.9.1-staging.ee54ba693a",
63
+ "@dxos/cli-util": "0.9.1-staging.ee54ba693a",
64
+ "@dxos/client": "0.9.1-staging.ee54ba693a",
65
+ "@dxos/app-toolkit": "0.9.1-staging.ee54ba693a",
66
+ "@dxos/client-protocol": "0.9.1-staging.ee54ba693a",
67
+ "@dxos/compute": "0.9.1-staging.ee54ba693a",
68
+ "@dxos/context": "0.9.1-staging.ee54ba693a",
69
+ "@dxos/edge-client": "0.9.1-staging.ee54ba693a",
70
+ "@dxos/echo": "0.9.1-staging.ee54ba693a",
71
+ "@dxos/invariant": "0.9.1-staging.ee54ba693a",
72
+ "@dxos/effect": "0.9.1-staging.ee54ba693a",
73
+ "@dxos/log": "0.9.1-staging.ee54ba693a",
74
+ "@dxos/plugin-graph": "0.9.1-staging.ee54ba693a",
75
+ "@dxos/keys": "0.9.1-staging.ee54ba693a",
76
+ "@dxos/protocols": "0.9.1-staging.ee54ba693a",
77
+ "@dxos/plugin-observability": "0.9.1-staging.ee54ba693a",
78
+ "@dxos/react-ui-form": "0.9.1-staging.ee54ba693a",
79
+ "@dxos/react-ui-markdown": "0.9.1-staging.ee54ba693a",
80
+ "@dxos/types": "0.9.1-staging.ee54ba693a",
81
+ "@dxos/react-ui-mosaic": "0.9.1-staging.ee54ba693a",
82
+ "@dxos/react-ui-list": "0.9.1-staging.ee54ba693a",
83
+ "@dxos/util": "0.9.1-staging.ee54ba693a"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@effect-atom/atom-react": "^0.5.0",
87
- "@effect/platform": "0.96.1",
88
- "@types/react": "~19.2.7",
87
+ "@effect/platform": "0.96.2",
88
+ "@types/react": "~19.2.17",
89
89
  "@types/react-dom": "~19.2.3",
90
- "effect": "3.21.3",
91
- "react": "~19.2.3",
92
- "react-dom": "~19.2.3",
90
+ "effect": "3.21.4",
91
+ "react": "~19.2.7",
92
+ "react-dom": "~19.2.7",
93
93
  "vite": "^8.0.16",
94
- "@dxos/plugin-testing": "0.9.1-main.c7dcc2e112",
95
- "@dxos/react-ui": "0.9.1-main.c7dcc2e112",
96
- "@dxos/storybook-utils": "0.9.1-main.c7dcc2e112",
97
- "@dxos/ui-theme": "0.9.1-main.c7dcc2e112",
98
- "@dxos/random": "0.9.1-main.c7dcc2e112"
94
+ "@dxos/random": "0.9.1-staging.ee54ba693a",
95
+ "@dxos/plugin-testing": "0.9.1-staging.ee54ba693a",
96
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a",
97
+ "@dxos/react-ui": "0.9.1-staging.ee54ba693a",
98
+ "@dxos/storybook-utils": "0.9.1-staging.ee54ba693a"
99
99
  },
100
100
  "peerDependencies": {
101
101
  "@effect-atom/atom-react": "^0.5.0",
102
- "@effect/platform": "0.96.1",
103
- "effect": "3.21.3",
104
- "react": "~19.2.3",
105
- "react-dom": "~19.2.3",
106
- "@dxos/react-ui": "0.9.1-main.c7dcc2e112",
107
- "@dxos/ui-theme": "0.9.1-main.c7dcc2e112"
102
+ "@effect/platform": "0.96.2",
103
+ "effect": "3.21.4",
104
+ "react": "~19.2.7",
105
+ "react-dom": "~19.2.7",
106
+ "@dxos/react-ui": "0.9.1-staging.ee54ba693a",
107
+ "@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
108
108
  },
109
109
  "publishConfig": {
110
110
  "access": "public"
@@ -9,8 +9,9 @@ import { AppCapabilities, LayoutOperation, SettingsOperation } from '@dxos/app-t
9
9
  import { Operation } from '@dxos/compute';
10
10
  import { DXN } from '@dxos/keys';
11
11
  import { GraphBuilder, Node, NodeMatcher } from '@dxos/plugin-graph';
12
+ import { Position } from '@dxos/util';
12
13
 
13
- import { REGISTRY_ID, REGISTRY_KEY, registryCategoryId, meta } from '#meta';
14
+ import { REGISTRY_ID, REGISTRY_KEY, meta, registryCategoryId } from '#meta';
14
15
 
15
16
  import { getCategoryPredicate, getRemotePluginIds } from '../categories';
16
17
  import { LOAD_PLUGIN_DIALOG } from '../containers';
@@ -82,7 +83,7 @@ export default Capability.makeModule(
82
83
  label: ['plugin-registry.label', { ns: meta.profile.key }],
83
84
  icon: 'ph--squares-four--regular',
84
85
  disposition: 'pin-end',
85
- position: 'first',
86
+ position: Position.first,
86
87
  testId: 'treeView.pluginRegistry',
87
88
  },
88
89
  nodes: [
@@ -17,7 +17,7 @@ import { AUTH_OPTION_DESCRIPTIONS, NSID, putRecord, resolveSession } from './uti
17
17
 
18
18
  /**
19
19
  * `dx registry publish-package` — publishes both the mutable
20
- * `package.profile` (rkey = key) and a `package.release`
20
+ * `plugin.profile` (rkey = key) and a `plugin.release`
21
21
  * (rkey = `<key>:<version>`) record to the authenticated user's PDS.
22
22
  *
23
23
  * The two writes are independent `putRecord` (upsert) calls; if the second fails the
@@ -94,12 +94,12 @@ export const publishPackage = Command.make(
94
94
  createdAt,
95
95
  };
96
96
 
97
- const profileResult = yield* putRecord(session, NSID.PackageProfile, options.key, profile);
97
+ const profileResult = yield* putRecord(session, NSID.PluginProfile, options.key, profile);
98
98
  yield* Console.log(`Profile ${profileResult.uri}`);
99
99
 
100
100
  const manifestHash = Option.getOrUndefined(options.manifestHash);
101
- const release: PluginRelease & { package: string; createdAt: string } = {
102
- package: options.key,
101
+ const release: PluginRelease & { pluginKey: string; manifestHash?: string; createdAt: string } = {
102
+ pluginKey: options.key,
103
103
  version: options.version,
104
104
  moduleUrl: options.moduleUrl,
105
105
  createdAt,
@@ -108,7 +108,7 @@ export const publishPackage = Command.make(
108
108
 
109
109
  const releaseResult = yield* putRecord(
110
110
  session,
111
- NSID.PackageRelease,
111
+ NSID.PluginRelease,
112
112
  `${options.key}:${options.version}`,
113
113
  release,
114
114
  );
@@ -43,8 +43,8 @@ const sha256Base64 = async (bytes: Uint8Array): Promise<string> => {
43
43
  * Reads the build/publish orchestration from `dx.config.ts`, runs the declared build command,
44
44
  * reads the emitted `manifest.json`, hosts the bundle (default: upload the build
45
45
  * output to the DXOS edge registry; override with `publish.assetBaseUrl` to point
46
- * at your own already-hosted directory), then writes the `package.profile` and
47
- * `package.release` records to the authenticated publisher's PDS. Release
46
+ * at your own already-hosted directory), then writes the `plugin.profile` and
47
+ * `plugin.release` records to the authenticated publisher's PDS. Release
48
48
  * integrity is anchored by `manifestHash` (sha256 of `manifest.json`) in the
49
49
  * signed release record.
50
50
  */
@@ -181,11 +181,11 @@ export const publish = Command.make(
181
181
  profile.spec = manifest.spec;
182
182
  }
183
183
 
184
- const profileResult = yield* putRecord(session, NSID.PackageProfile, key, profile);
184
+ const profileResult = yield* putRecord(session, NSID.PluginProfile, key, profile);
185
185
  yield* Console.log(`Profile ${profileResult.uri}`);
186
186
 
187
- const releaseResult = yield* putRecord(session, NSID.PackageRelease, `${key}:${version}`, {
188
- package: key,
187
+ const releaseResult = yield* putRecord(session, NSID.PluginRelease, `${key}:${version}`, {
188
+ pluginKey: key,
189
189
  version,
190
190
  moduleUrl,
191
191
  manifestHash,
@@ -13,7 +13,7 @@ import * as Option from 'effect/Option';
13
13
  import { CommandConfig } from '@dxos/cli-util';
14
14
  import { ClientService } from '@dxos/client';
15
15
 
16
- import { ALL_NSIDS, AUTH_OPTION_DESCRIPTIONS, listRecords, resolveSession, type ListRecordsEntry } from './util';
16
+ import { ALL_NSIDS, AUTH_OPTION_DESCRIPTIONS, type ListRecordsEntry, listRecords, resolveSession } from './util';
17
17
 
18
18
  /**
19
19
  * `dx registry records` — lists all `org.dxos.experimental.*` records on the
@@ -17,8 +17,8 @@ import { AUTH_OPTION_DESCRIPTIONS, NSID, deleteRecord, listRecords, resolveSessi
17
17
  const rkeyOf = (uri: string): string => uri.split('/').pop() ?? '';
18
18
 
19
19
  /**
20
- * `dx registry unpublish` — removes a package from the authenticated user's PDS:
21
- * deletes the `package.profile` (rkey = key) and every `package.release`
20
+ * `dx registry unpublish` — removes a plugin from the authenticated user's PDS:
21
+ * deletes the `plugin.profile` (rkey = key) and every `plugin.release`
22
22
  * (rkey = `<key>:<version>`). The registry indexer unindexes on its next sweep.
23
23
  * Hosted bundles in edge R2 are immutable and are not removed.
24
24
  */
@@ -31,7 +31,7 @@ export const unpublish = Command.make(
31
31
  Options.optional,
32
32
  ),
33
33
  key: Options.text('key').pipe(
34
- Options.withDescription('Package key (the profile rkey). Removes its profile and all releases.'),
34
+ Options.withDescription('Plugin key (the profile rkey). Removes its profile and all releases.'),
35
35
  ),
36
36
  },
37
37
  (options) =>
@@ -49,12 +49,12 @@ export const unpublish = Command.make(
49
49
  let cursor: string | undefined;
50
50
  let deletedReleases = 0;
51
51
  do {
52
- const releases = yield* listRecords(session, NSID.PackageRelease, { limit: 100, cursor });
52
+ const releases = yield* listRecords(session, NSID.PluginRelease, { limit: 100, cursor });
53
53
  cursor = releases.cursor;
54
54
  for (const record of releases.records) {
55
55
  const rkey = rkeyOf(record.uri);
56
56
  if (rkey.startsWith(`${key}:`)) {
57
- yield* deleteRecord(session, NSID.PackageRelease, rkey);
57
+ yield* deleteRecord(session, NSID.PluginRelease, rkey);
58
58
  yield* Console.log(`Deleted release ${rkey}`);
59
59
  deletedReleases += 1;
60
60
  }
@@ -62,7 +62,7 @@ export const unpublish = Command.make(
62
62
  } while (cursor !== undefined);
63
63
 
64
64
  // Delete the profile (rkey = key).
65
- yield* deleteRecord(session, NSID.PackageProfile, key);
65
+ yield* deleteRecord(session, NSID.PluginProfile, key);
66
66
  yield* Console.log(`Deleted profile ${key} (${deletedReleases} release(s) removed)`);
67
67
  }),
68
68
  Effect.provide(FetchHttpClient.layer),
@@ -32,7 +32,7 @@ export { ALL_NSIDS, NSID };
32
32
  */
33
33
 
34
34
  // `AccessToken.source` of the default atproto / login integration ("Atmosphere"). Mirrors
35
- // `ATMOSPHERE_SOURCE` in plugin-integration; inlined to avoid a plugin-registry -> plugin-integration
35
+ // `ATMOSPHERE_SOURCE` in plugin-connector; inlined to avoid a plugin-registry -> plugin-connector
36
36
  // dependency.
37
37
  const ATMOSPHERE_SOURCE = 'atproto.local';
38
38
 
@@ -13,6 +13,23 @@ import { translations } from '#translations';
13
13
 
14
14
  import { PluginDetail } from './PluginDetail';
15
15
 
16
+ const plugin = Plugin.define(
17
+ Plugin.makeMeta({
18
+ key: DXN.make('com.example.plugin.test'),
19
+ name: 'Test Plugin',
20
+ author: 'DXOS',
21
+ description: random.lorem.paragraphs(2),
22
+ icon: { key: 'ph--bug--regular', hue: 'sky' },
23
+ homePage: 'https://example.com',
24
+ source: 'https://github.com/example/test-plugin',
25
+ screenshots: [
26
+ {
27
+ light: 'https://placehold.co/1728x990',
28
+ },
29
+ ],
30
+ }),
31
+ );
32
+
16
33
  const meta = {
17
34
  title: 'plugins/plugin-registry/components/PluginDetail',
18
35
  component: PluginDetail,
@@ -29,22 +46,19 @@ type Story = StoryObj<typeof meta>;
29
46
 
30
47
  export const Default: Story = {
31
48
  args: {
32
- plugin: Plugin.define(
33
- Plugin.makeMeta({
34
- key: DXN.make('com.example.plugin.test'),
35
- name: 'Test Plugin',
36
- author: 'DXOS',
37
- description: random.lorem.paragraphs(2),
38
- icon: { key: 'ph--bug--regular', hue: 'sky' },
39
- homePage: 'https://example.com',
40
- source: 'https://github.com/example/test-plugin',
41
- screenshots: [
42
- {
43
- light:
44
- 'https://media.gcflearnfree.org/content/55e073de7dd48174331f51b3_01_17_2014/getting_started_interactive2.png',
45
- },
46
- ],
47
- }),
48
- ).pipe(Plugin.make)(),
49
+ plugin: plugin.pipe(Plugin.make)(),
50
+ },
51
+ };
52
+
53
+ export const Failure: Story = {
54
+ args: {
55
+ plugin: plugin.pipe(Plugin.make)(),
56
+ failure: {
57
+ id: 'com.example.plugin.test',
58
+ phase: 'activation',
59
+ reason: 'error',
60
+ error: new Error(random.lorem.paragraph(2)),
61
+ timestamp: Date.now(),
62
+ },
49
63
  },
50
64
  };
@@ -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) => {
29
+ export const PluginFailureBadge = ({ failure, size }: PluginFailureBadgeProps) => {
32
30
  const { t } = useTranslation(meta.profile.key);
33
31
 
34
- const phaseLabel = failure.phase === 'load' ? t('failure-phase-load.label') : t('failure-phase-activation.label');
35
- const reasonLabel =
36
- failure.reason === 'timeout' ? t('failure-reason-timeout.label') : t('failure-reason-error.label');
37
-
38
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
 
@@ -99,7 +99,6 @@ export const PluginItem = ({
99
99
  const showInstallButton = !!onInstall && !isInstalled;
100
100
  const showUpdateButton = !!onUpdate && isInstalled && !!hasUpdate;
101
101
  const inputId = `${id}-input`;
102
- const labelId = `${id}-label`;
103
102
  const descriptionId = `${id}-description`;
104
103
  const handleClick = useCallback(() => onClick?.(id), [id, onClick]);
105
104
 
@@ -134,12 +133,16 @@ export const PluginItem = ({
134
133
  const gridRows = 'grid grid-cols-1 grid-rows-[40px_1fr_min-content_40px]';
135
134
 
136
135
  return (
137
- <ListItem.Root
138
- key={id}
139
- labelId={labelId}
136
+ <Listbox.Item
137
+ id={id}
140
138
  data-testid={`pluginList.${id}`}
141
139
  aria-describedby={descriptionId}
142
- 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
+ )}
143
146
  >
144
147
  <div className={mx(gridRows, 'rounded-l-md', styles.surface)}>
145
148
  <div className='flex justify-center row-start-2 cursor-pointer' onClick={handleClick}>
@@ -159,7 +162,7 @@ export const PluginItem = ({
159
162
 
160
163
  <div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
161
164
  {displayTags.map((tag: string) => (
162
- <Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase'>
165
+ <Tag key={tag} hue={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase'>
163
166
  {tag}
164
167
  </Tag>
165
168
  ))}
@@ -209,7 +212,7 @@ export const PluginItem = ({
209
212
  </div>
210
213
  </div>
211
214
  </div>
212
- </ListItem.Root>
215
+ </Listbox.Item>
213
216
  );
214
217
  };
215
218
 
@@ -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.profile.key}
43
- plugin={plugin}
44
- extraTags={extraTagsById?.[plugin.meta.profile.key]}
45
- hasUpdate={updateAvailableIds?.has(plugin.meta.profile.key)}
46
- failure={failuresById?.[plugin.meta.profile.key]}
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,
@@ -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
  };
@@ -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';
@@ -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';