@dxos/plugin-client 0.9.1-main.c7dcc2e112 → 0.10.0

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 (131) hide show
  1. package/dist/lib/neutral/{AccountContainer-LIMGLO2Y.mjs → AccountContainer-7SF53UON.mjs} +15 -11
  2. package/dist/lib/neutral/AccountContainer-7SF53UON.mjs.map +7 -0
  3. package/dist/lib/neutral/ClientPlugin.node.mjs +2 -2
  4. package/dist/lib/neutral/ClientPlugin.node.mjs.map +3 -3
  5. package/dist/lib/neutral/{DevicesContainer-X26OGLFK.mjs → DevicesContainer-PRWIN36N.mjs} +25 -16
  6. package/dist/lib/neutral/DevicesContainer-PRWIN36N.mjs.map +7 -0
  7. package/dist/lib/neutral/{InvitationsContainer-OBBCJ7CJ.mjs → InvitationsContainer-BI5RAWET.mjs} +42 -31
  8. package/dist/lib/neutral/InvitationsContainer-BI5RAWET.mjs.map +7 -0
  9. package/dist/lib/neutral/{ProfileContainer-FGS2MVWA.mjs → ProfileContainer-YEAYOTX5.mjs} +21 -20
  10. package/dist/lib/neutral/ProfileContainer-YEAYOTX5.mjs.map +7 -0
  11. package/dist/lib/neutral/{RecoveryCredentialsContainer-SL4LYHBE.mjs → RecoveryCredentialsContainer-6BZ2IWX7.mjs} +22 -13
  12. package/dist/lib/neutral/RecoveryCredentialsContainer-6BZ2IWX7.mjs.map +7 -0
  13. package/dist/lib/neutral/UsageContainer-2DWCVVE4.mjs +241 -0
  14. package/dist/lib/neutral/UsageContainer-2DWCVVE4.mjs.map +7 -0
  15. package/dist/lib/neutral/account-cache-YVQKLNV2.mjs +16 -0
  16. package/dist/lib/neutral/account-cache-YVQKLNV2.mjs.map +7 -0
  17. package/dist/lib/neutral/{app-graph-builder-FTXJNX37.mjs → app-graph-builder-RVXTD6OE.mjs} +3 -3
  18. package/dist/lib/neutral/app-graph-builder-RVXTD6OE.mjs.map +7 -0
  19. package/dist/lib/neutral/capabilities/index.mjs +4 -4
  20. package/dist/lib/neutral/capabilities/node.mjs +3 -3
  21. package/dist/lib/neutral/chunk-A7WDDIWY.mjs +162 -0
  22. package/dist/lib/neutral/chunk-A7WDDIWY.mjs.map +7 -0
  23. package/dist/lib/neutral/{chunk-JEF2FQGB.mjs → chunk-JY35S2CN.mjs} +2 -2
  24. package/dist/lib/neutral/{chunk-JEF2FQGB.mjs.map → chunk-JY35S2CN.mjs.map} +1 -1
  25. package/dist/lib/neutral/chunk-ZFABGG2E.mjs +8 -0
  26. package/dist/lib/neutral/containers/index.mjs +6 -6
  27. package/dist/lib/neutral/{create-agent-U567RM7Z.mjs → create-agent-6UKJMZS5.mjs} +2 -2
  28. package/dist/lib/neutral/{create-identity-IKN2FJ5B.mjs → create-identity-ULCRB3YH.mjs} +2 -2
  29. package/dist/lib/neutral/{create-passkey-JAD3ACY7.mjs → create-passkey-I7VDS3AC.mjs} +2 -2
  30. package/dist/lib/neutral/{create-recovery-code-JZYXR4BY.mjs → create-recovery-code-DH2B6EGP.mjs} +2 -2
  31. package/dist/lib/neutral/index.mjs +4 -2
  32. package/dist/lib/neutral/{layer-specs-VQCJLV33.mjs → layer-specs-YI7BVWKZ.mjs} +6 -8
  33. package/dist/lib/neutral/layer-specs-YI7BVWKZ.mjs.map +7 -0
  34. package/dist/lib/neutral/meta.json +1 -1
  35. package/dist/lib/neutral/{navigation-handler-MPJAVHWY.mjs → navigation-handler-MQ6MZXLX.mjs} +2 -2
  36. package/dist/lib/neutral/{open-usage-24I7RAFL.mjs → open-usage-OJVTPGOY.mjs} +2 -2
  37. package/dist/lib/neutral/operations/index.mjs +1 -1
  38. package/dist/lib/neutral/plugin.mjs +1 -1
  39. package/dist/lib/neutral/{redeem-passkey-4DR7GPMU.mjs → redeem-passkey-TYP4XLHO.mjs} +2 -2
  40. package/dist/lib/neutral/{redeem-token-N5MBQG3I.mjs → redeem-token-2J33R3PV.mjs} +2 -2
  41. package/dist/lib/neutral/{share-identity-YJIFDIPJ.mjs → share-identity-TOROBLWL.mjs} +2 -2
  42. package/dist/lib/neutral/testing/index.mjs.map +2 -2
  43. package/dist/lib/neutral/types/index.mjs +3 -1
  44. package/dist/types/src/capabilities/account-cache.d.ts +1 -31
  45. package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -2
  46. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  47. package/dist/types/src/capabilities/client.d.ts +1 -2
  48. package/dist/types/src/capabilities/client.d.ts.map +1 -1
  49. package/dist/types/src/capabilities/hub-http-client.d.ts +1 -2
  50. package/dist/types/src/capabilities/hub-http-client.d.ts.map +1 -1
  51. package/dist/types/src/capabilities/layer-specs.d.ts +1 -2
  52. package/dist/types/src/capabilities/layer-specs.d.ts.map +1 -1
  53. package/dist/types/src/capabilities/migrations.d.ts +1 -1
  54. package/dist/types/src/capabilities/navigation-handler/navigation-handler.d.ts +1 -2
  55. package/dist/types/src/capabilities/navigation-handler/navigation-handler.d.ts.map +1 -1
  56. package/dist/types/src/capabilities/react-context.d.ts +1 -6
  57. package/dist/types/src/capabilities/react-context.d.ts.map +1 -1
  58. package/dist/types/src/capabilities/react-surface.d.ts +1 -1
  59. package/dist/types/src/capabilities/schema-defs.d.ts +1 -1
  60. package/dist/types/src/containers/AccountContainer/AccountContainer.d.ts.map +1 -1
  61. package/dist/types/src/containers/AccountContainer/AccountContainer.stories.d.ts +14 -0
  62. package/dist/types/src/containers/AccountContainer/AccountContainer.stories.d.ts.map +1 -0
  63. package/dist/types/src/containers/DevicesContainer/DevicesContainer.d.ts.map +1 -1
  64. package/dist/types/src/containers/InvitationsContainer/InvitationsContainer.d.ts.map +1 -1
  65. package/dist/types/src/containers/InvitationsContainer/InvitationsContainer.stories.d.ts +14 -0
  66. package/dist/types/src/containers/InvitationsContainer/InvitationsContainer.stories.d.ts.map +1 -0
  67. package/dist/types/src/containers/ProfileContainer/ProfileContainer.d.ts.map +1 -1
  68. package/dist/types/src/containers/RecoveryCredentialsContainer/RecoveryCredentialsContainer.d.ts.map +1 -1
  69. package/dist/types/src/containers/UsageContainer/UsageContainer.d.ts.map +1 -1
  70. package/dist/types/src/containers/UsageContainer/UsageView.d.ts +16 -7
  71. package/dist/types/src/containers/UsageContainer/UsageView.d.ts.map +1 -1
  72. package/dist/types/src/containers/UsageContainer/UsageView.stories.d.ts +2 -0
  73. package/dist/types/src/containers/UsageContainer/UsageView.stories.d.ts.map +1 -1
  74. package/dist/types/src/hooks/index.d.ts +2 -0
  75. package/dist/types/src/hooks/index.d.ts.map +1 -0
  76. package/dist/types/src/{state/use-hub-http.d.ts → hooks/useHubClient.d.ts} +1 -1
  77. package/dist/types/src/hooks/useHubClient.d.ts.map +1 -0
  78. package/dist/types/src/meta.d.ts +4 -0
  79. package/dist/types/src/meta.d.ts.map +1 -1
  80. package/dist/types/src/testing/initializeIdentity.d.ts.map +1 -1
  81. package/dist/types/src/{state → types}/account-cache.d.ts.map +1 -1
  82. package/dist/types/src/types/index.d.ts +1 -0
  83. package/dist/types/src/types/index.d.ts.map +1 -1
  84. package/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/package.json +46 -45
  86. package/src/capabilities/account-cache.ts +1 -3
  87. package/src/capabilities/app-graph-builder.ts +80 -79
  88. package/src/capabilities/layer-specs.ts +4 -6
  89. package/src/commands/account/login/login.ts +2 -2
  90. package/src/containers/AccountContainer/AccountContainer.stories.tsx +46 -0
  91. package/src/containers/AccountContainer/AccountContainer.tsx +66 -62
  92. package/src/containers/DevicesContainer/DevicesContainer.tsx +69 -54
  93. package/src/containers/InvitationsContainer/InvitationsContainer.stories.tsx +46 -0
  94. package/src/containers/InvitationsContainer/InvitationsContainer.tsx +64 -61
  95. package/src/containers/ProfileContainer/ProfileContainer.tsx +37 -27
  96. package/src/containers/RecoveryCredentialsContainer/RecoveryCredentialsContainer.tsx +50 -41
  97. package/src/containers/UsageContainer/UsageContainer.tsx +11 -10
  98. package/src/containers/UsageContainer/UsageView.stories.tsx +13 -0
  99. package/src/containers/UsageContainer/UsageView.tsx +182 -150
  100. package/src/hooks/index.ts +5 -0
  101. package/src/testing/initializeIdentity.ts +1 -0
  102. package/src/types/capabilities.ts +1 -1
  103. package/src/types/index.ts +1 -0
  104. package/dist/lib/neutral/AccountContainer-LIMGLO2Y.mjs.map +0 -7
  105. package/dist/lib/neutral/DevicesContainer-X26OGLFK.mjs.map +0 -7
  106. package/dist/lib/neutral/InvitationsContainer-OBBCJ7CJ.mjs.map +0 -7
  107. package/dist/lib/neutral/ProfileContainer-FGS2MVWA.mjs.map +0 -7
  108. package/dist/lib/neutral/RecoveryCredentialsContainer-SL4LYHBE.mjs.map +0 -7
  109. package/dist/lib/neutral/UsageContainer-HBPMLIK7.mjs +0 -226
  110. package/dist/lib/neutral/UsageContainer-HBPMLIK7.mjs.map +0 -7
  111. package/dist/lib/neutral/account-cache-4EKGEUJI.mjs +0 -43
  112. package/dist/lib/neutral/account-cache-4EKGEUJI.mjs.map +0 -7
  113. package/dist/lib/neutral/app-graph-builder-FTXJNX37.mjs.map +0 -7
  114. package/dist/lib/neutral/chunk-54MDDWCV.mjs +0 -8
  115. package/dist/lib/neutral/chunk-DA3LZQSD.mjs +0 -136
  116. package/dist/lib/neutral/chunk-DA3LZQSD.mjs.map +0 -7
  117. package/dist/lib/neutral/layer-specs-VQCJLV33.mjs.map +0 -7
  118. package/dist/types/src/state/use-hub-http.d.ts.map +0 -1
  119. /package/dist/lib/neutral/{chunk-54MDDWCV.mjs.map → chunk-ZFABGG2E.mjs.map} +0 -0
  120. /package/dist/lib/neutral/{create-agent-U567RM7Z.mjs.map → create-agent-6UKJMZS5.mjs.map} +0 -0
  121. /package/dist/lib/neutral/{create-identity-IKN2FJ5B.mjs.map → create-identity-ULCRB3YH.mjs.map} +0 -0
  122. /package/dist/lib/neutral/{create-passkey-JAD3ACY7.mjs.map → create-passkey-I7VDS3AC.mjs.map} +0 -0
  123. /package/dist/lib/neutral/{create-recovery-code-JZYXR4BY.mjs.map → create-recovery-code-DH2B6EGP.mjs.map} +0 -0
  124. /package/dist/lib/neutral/{navigation-handler-MPJAVHWY.mjs.map → navigation-handler-MQ6MZXLX.mjs.map} +0 -0
  125. /package/dist/lib/neutral/{open-usage-24I7RAFL.mjs.map → open-usage-OJVTPGOY.mjs.map} +0 -0
  126. /package/dist/lib/neutral/{redeem-passkey-4DR7GPMU.mjs.map → redeem-passkey-TYP4XLHO.mjs.map} +0 -0
  127. /package/dist/lib/neutral/{redeem-token-N5MBQG3I.mjs.map → redeem-token-2J33R3PV.mjs.map} +0 -0
  128. /package/dist/lib/neutral/{share-identity-YJIFDIPJ.mjs.map → share-identity-TOROBLWL.mjs.map} +0 -0
  129. /package/dist/types/src/{state → types}/account-cache.d.ts +0 -0
  130. /package/src/{state/use-hub-http.ts → hooks/useHubClient.ts} +0 -0
  131. /package/src/{state → types}/account-cache.ts +0 -0
@@ -11,8 +11,9 @@ import { useClient, useMulticastObservable } from '@dxos/react-client';
11
11
  import { type Device, useDevices } from '@dxos/react-client/halo';
12
12
  import { type CancellableInvitationObservable, Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
13
13
  import { useNetworkStatus } from '@dxos/react-client/mesh';
14
- import { Button, Clipboard, Icon, IconButton, List, useId, useTranslation } from '@dxos/react-ui';
15
- import { Settings } from '@dxos/react-ui-form';
14
+ import { Button, Clipboard, Icon, IconButton, useId, useTranslation } from '@dxos/react-ui';
15
+ import { Form } from '@dxos/react-ui-form';
16
+ import { Listbox } from '@dxos/react-ui-list';
16
17
  import { AuthCode, Centered, DeviceListItem, Emoji, Viewport } from '@dxos/shell/react';
17
18
  import { osTranslations } from '@dxos/ui-theme';
18
19
  import { hexToEmoji } from '@dxos/util';
@@ -44,58 +45,72 @@ export const DevicesContainer = ({ createInvitationUrl }: DevicesContainerProps)
44
45
 
45
46
  return (
46
47
  <Clipboard.Provider>
47
- <Settings.Viewport>
48
- <Settings.Section
49
- title={t('devices-verbose.label', { ns: meta.profile.key })}
50
- description={t('devices.description', { ns: meta.profile.key })}
51
- >
52
- <Settings.Panel>
53
- <div role='group' className='min-w-0'>
54
- <h3 className='text-lg mb-2'>{t('devices.label', { ns: meta.profile.key })}</h3>
55
- <List>
56
- {devices.map((device: Device) => (
57
- <DeviceListItem key={device.deviceKey.toHex()} device={device} connectionState={connectionState} />
58
- ))}
59
- </List>
60
- </div>
61
- {createInvitationUrl && (
62
- <div role='group' className='min-w-0'>
63
- <h3 className='text-lg mb-2'>{t('add-device.label')}</h3>
64
- <DeviceInvitation createInvitationUrl={createInvitationUrl} />
65
- </div>
66
- )}
67
- </Settings.Panel>
68
- </Settings.Section>
69
- <Settings.Section
70
- title={t('danger-zone.title', { ns: meta.profile.key })}
71
- description={t('danger-zone.description', { ns: meta.profile.key })}
72
- >
73
- <Settings.Item
74
- title={t('reset-device.label')}
75
- description={t('reset-device.description', { ns: meta.profile.key })}
76
- >
77
- <Button variant='destructive' onClick={handleResetStorage} data-testid='devicesContainer.reset'>
78
- {t('reset-device.label')}
79
- </Button>
80
- </Settings.Item>
81
- <Settings.Item
82
- title={t('recover-identity.label')}
83
- description={t('recover-identity.description', { ns: meta.profile.key })}
84
- >
85
- <Button variant='destructive' onClick={handleRecover} data-testid='devicesContainer.recover'>
86
- {t('recover-identity.label')}
87
- </Button>
88
- </Settings.Item>
89
- <Settings.Item
90
- title={t('join-new-identity.label')}
91
- description={t('join-new-identity.description', { ns: meta.profile.key })}
92
- >
93
- <Button variant='destructive' onClick={handleJoinNewIdentity} data-testid='devicesContainer.joinExisting'>
94
- {t('join-new-identity.label')}
95
- </Button>
96
- </Settings.Item>
97
- </Settings.Section>
98
- </Settings.Viewport>
48
+ <Form.Root variant='settings'>
49
+ <Form.Viewport scroll>
50
+ <Form.Content>
51
+ <Form.Section
52
+ title={t('devices-verbose.label', { ns: meta.profile.key })}
53
+ description={t('devices.description', { ns: meta.profile.key })}
54
+ >
55
+ <Form.Group>
56
+ <div role='group' className='min-w-0'>
57
+ <h3 className='text-lg mb-2'>{t('devices.label', { ns: meta.profile.key })}</h3>
58
+ <Listbox.Root>
59
+ <Listbox.Content aria-label={t('devices.label', { ns: meta.profile.key })}>
60
+ {devices.map((device: Device) => (
61
+ <DeviceListItem
62
+ key={device.deviceKey.toHex()}
63
+ device={device}
64
+ connectionState={connectionState}
65
+ />
66
+ ))}
67
+ </Listbox.Content>
68
+ </Listbox.Root>
69
+ </div>
70
+ {createInvitationUrl && (
71
+ <div role='group' className='min-w-0'>
72
+ <h3 className='text-lg mb-2'>{t('add-device.label')}</h3>
73
+ <DeviceInvitation createInvitationUrl={createInvitationUrl} />
74
+ </div>
75
+ )}
76
+ </Form.Group>
77
+ </Form.Section>
78
+ <Form.Section
79
+ title={t('danger-zone.title', { ns: meta.profile.key })}
80
+ description={t('danger-zone.description', { ns: meta.profile.key })}
81
+ >
82
+ <Form.Row
83
+ label={t('reset-device.label')}
84
+ description={t('reset-device.description', { ns: meta.profile.key })}
85
+ >
86
+ <Button variant='destructive' onClick={handleResetStorage} data-testid='devicesContainer.reset'>
87
+ {t('reset-device.label')}
88
+ </Button>
89
+ </Form.Row>
90
+ <Form.Row
91
+ label={t('recover-identity.label')}
92
+ description={t('recover-identity.description', { ns: meta.profile.key })}
93
+ >
94
+ <Button variant='destructive' onClick={handleRecover} data-testid='devicesContainer.recover'>
95
+ {t('recover-identity.label')}
96
+ </Button>
97
+ </Form.Row>
98
+ <Form.Row
99
+ label={t('join-new-identity.label')}
100
+ description={t('join-new-identity.description', { ns: meta.profile.key })}
101
+ >
102
+ <Button
103
+ variant='destructive'
104
+ onClick={handleJoinNewIdentity}
105
+ data-testid='devicesContainer.joinExisting'
106
+ >
107
+ {t('join-new-identity.label')}
108
+ </Button>
109
+ </Form.Row>
110
+ </Form.Section>
111
+ </Form.Content>
112
+ </Form.Viewport>
113
+ </Form.Root>
99
114
  </Clipboard.Provider>
100
115
  );
101
116
  };
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { ProcessManagerPlugin } from '@dxos/app-framework';
9
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { initializeIdentity } from '#testing';
13
+ import { translations } from '#translations';
14
+
15
+ import { ClientPlugin } from '../../ClientPlugin';
16
+ import { InvitationsContainer } from './InvitationsContainer';
17
+
18
+ const meta = {
19
+ title: 'plugins/plugin-client/containers/InvitationsContainer',
20
+ component: InvitationsContainer,
21
+ decorators: [
22
+ withTheme(),
23
+ withLayout({ layout: 'fullscreen' }),
24
+ withPluginManager({
25
+ plugins: [
26
+ ClientPlugin({
27
+ onClientInitialized: ({ client }) =>
28
+ Effect.gen(function* () {
29
+ yield* initializeIdentity(client);
30
+ }),
31
+ }),
32
+ ProcessManagerPlugin(),
33
+ ],
34
+ }),
35
+ ],
36
+ parameters: {
37
+ layout: 'fullscreen',
38
+ translations,
39
+ },
40
+ } satisfies Meta<typeof InvitationsContainer>;
41
+
42
+ export default meta;
43
+
44
+ type Story = StoryObj<typeof meta>;
45
+
46
+ export const Default: Story = {};
@@ -7,14 +7,14 @@ import React, { useCallback, useState } from 'react';
7
7
 
8
8
  import { useCapability } from '@dxos/app-framework/ui';
9
9
  import { Context } from '@dxos/context';
10
- import { Clipboard, Icon, IconButton, List, ListItem, useAsyncEffect, useTranslation } from '@dxos/react-ui';
11
- import { Settings } from '@dxos/react-ui-form';
10
+ import { Clipboard, Icon, IconButton, useAsyncEffect, useTranslation } from '@dxos/react-ui';
11
+ import { Form } from '@dxos/react-ui-form';
12
+ import { Listbox } from '@dxos/react-ui-list';
12
13
 
13
14
  import { meta } from '#meta';
14
- import { ClientCapabilities } from '#types';
15
+ import { type AccountCacheInvitation, ClientCapabilities } from '#types';
15
16
 
16
- import { type AccountCacheInvitation } from '../../state/account-cache';
17
- import { useHubHttpClient } from '../../state/use-hub-http';
17
+ import { useHubHttpClient } from '../../hooks';
18
18
 
19
19
  export const InvitationsContainer = () => {
20
20
  const { t } = useTranslation(meta.profile.key);
@@ -24,27 +24,28 @@ export const InvitationsContainer = () => {
24
24
  const [pending, setPending] = useState(false);
25
25
 
26
26
  // Account/invitation routes live on hub-service, not the edge worker.
27
- const hubHttp = useHubHttpClient();
27
+ const hubClient = useHubHttpClient();
28
28
 
29
29
  useAsyncEffect(async () => {
30
- if (!hubHttp) {
30
+ if (!hubClient) {
31
31
  return;
32
32
  }
33
+
33
34
  try {
34
- const result = await hubHttp.listAccountInvitations(new Context());
35
+ const result = await hubClient.listAccountInvitations(new Context());
35
36
  setCache((prev) => ({ ...prev, invitations: result.invitations, fetchedAt: Date.now() }));
36
37
  } catch {
37
38
  // Offline: keep cache.
38
39
  }
39
- }, [hubHttp, setCache]);
40
+ }, [hubClient, setCache]);
40
41
 
41
42
  const handleIssue = useCallback(async () => {
42
- if (!hubHttp) {
43
+ if (!hubClient) {
43
44
  return;
44
45
  }
45
46
  setPending(true);
46
47
  try {
47
- const result = await hubHttp.issueAccountInvitation(new Context());
48
+ const result = await hubClient.issueAccountInvitation(new Context());
48
49
  // Optimistically push the new code and decrement the remaining quota; the
49
50
  // server consumes one slot at issue time. Next refresh reconciles.
50
51
  setCache((prev) => ({
@@ -57,7 +58,7 @@ export const InvitationsContainer = () => {
57
58
  } finally {
58
59
  setPending(false);
59
60
  }
60
- }, [hubHttp, setCache]);
61
+ }, [hubClient, setCache]);
61
62
 
62
63
  const remaining = cache.account?.invitationsRemaining ?? 0;
63
64
  const list = cache.invitations ?? [];
@@ -66,72 +67,74 @@ export const InvitationsContainer = () => {
66
67
 
67
68
  return (
68
69
  <Clipboard.Provider>
69
- <Settings.Viewport>
70
- <Settings.Section title={t('invitations-section.title')} description={t('invitations-section.description')}>
71
- <Settings.Item
72
- title={t('generate-invitation.label')}
73
- description={t('generate-invitation.description', { count: remaining })}
74
- >
75
- <IconButton
76
- icon='ph--plus--regular'
77
- label={t('generate-invitation.label')}
78
- variant='primary'
79
- onClick={handleIssue}
80
- disabled={pending || remaining <= 0}
81
- />
82
- </Settings.Item>
83
- </Settings.Section>
70
+ <Form.Root variant='settings'>
71
+ <Form.Viewport scroll>
72
+ <Form.Content>
73
+ <Form.Section title={t('invitations-section.title')} description={t('invitations-section.description')}>
74
+ <Form.Row
75
+ label={t('generate-invitation.label')}
76
+ description={t('generate-invitation.description', { count: remaining })}
77
+ >
78
+ <IconButton
79
+ icon='ph--plus--regular'
80
+ label={t('generate-invitation.label')}
81
+ variant='primary'
82
+ onClick={handleIssue}
83
+ disabled={pending || remaining <= 0}
84
+ />
85
+ </Form.Row>
86
+ </Form.Section>
84
87
 
85
- {available.length > 0 ? (
86
- <Settings.Section title={t('available-invitations.title')}>
87
- <List>
88
- {available.map((row) => (
89
- <AvailableInvitationItem key={row.code} row={row} />
90
- ))}
91
- </List>
92
- </Settings.Section>
93
- ) : null}
88
+ {available.length > 0 ? (
89
+ <Form.Section title={t('available-invitations.title')}>
90
+ <Listbox.Root>
91
+ <Listbox.Content classNames='gap-1'>
92
+ {available.map((row) => (
93
+ <AvailableInvitationItem key={row.code} row={row} />
94
+ ))}
95
+ </Listbox.Content>
96
+ </Listbox.Root>
97
+ </Form.Section>
98
+ ) : null}
94
99
 
95
- {redeemed.length > 0 ? (
96
- <Settings.Section title={t('redeemed-invitations.title')}>
97
- <List>
98
- {redeemed.map((row) => (
99
- <RedeemedInvitationItem key={row.code} row={row} />
100
- ))}
101
- </List>
102
- </Settings.Section>
103
- ) : null}
104
- </Settings.Viewport>
100
+ {redeemed.length > 0 ? (
101
+ <Form.Section title={t('redeemed-invitations.title')}>
102
+ <Listbox.Root>
103
+ <Listbox.Content classNames='gap-1'>
104
+ {redeemed.map((row) => (
105
+ <RedeemedInvitationItem key={row.code} row={row} />
106
+ ))}
107
+ </Listbox.Content>
108
+ </Listbox.Root>
109
+ </Form.Section>
110
+ ) : null}
111
+ </Form.Content>
112
+ </Form.Viewport>
113
+ </Form.Root>
105
114
  </Clipboard.Provider>
106
115
  );
107
116
  };
108
117
 
109
118
  const AvailableInvitationItem = ({ row }: { row: AccountCacheInvitation }) => (
110
- <ListItem.Root classNames='grid grid-cols-[min-content_1fr_min-content] items-center gap-2'>
111
- <ListItem.Endcap>
112
- <Icon icon='ph--paper-plane-tilt--duotone' size={5} classNames='text-description' />
113
- </ListItem.Endcap>
119
+ <Listbox.Item id={row.code} classNames='grid grid-cols-[min-content_1fr_min-content] items-center gap-2'>
120
+ <Icon icon='ph--paper-plane-tilt--duotone' size={5} classNames='text-description' />
114
121
  <div className='flex flex-col min-w-0'>
115
- <ListItem.Heading classNames='font-mono truncate'>{row.code}</ListItem.Heading>
122
+ <div className='font-mono truncate'>{row.code}</div>
116
123
  <p className='text-description text-xs'>{new Date(row.createdAt).toLocaleString()}</p>
117
124
  </div>
118
- <ListItem.Endcap>
119
- <Clipboard.IconButton value={row.code} />
120
- </ListItem.Endcap>
121
- </ListItem.Root>
125
+ <Clipboard.IconButton value={row.code} />
126
+ </Listbox.Item>
122
127
  );
123
128
 
124
129
  const RedeemedInvitationItem = ({ row }: { row: AccountCacheInvitation }) => {
125
130
  const date = row.redeemedAt ?? row.createdAt;
126
131
  return (
127
- <ListItem.Root classNames='grid grid-cols-[min-content_1fr] items-center gap-2'>
128
- <ListItem.Endcap>
129
- <Icon icon='ph--check-circle--duotone' size={5} classNames='text-success-text' />
130
- </ListItem.Endcap>
132
+ <Listbox.Item id={row.code} classNames='grid grid-cols-[min-content_1fr] items-center gap-2'>
133
+ <Icon icon='ph--check-circle--duotone' size={5} classNames='text-success-text' />
131
134
  <div className='flex flex-col min-w-0'>
132
- <ListItem.Heading classNames='font-mono truncate'>{row.code}</ListItem.Heading>
135
+ <div className='font-mono truncate'>{row.code}</div>
133
136
  <p className='text-description text-xs'>{new Date(date).toLocaleString()}</p>
134
137
  </div>
135
- </ListItem.Root>
138
+ </Listbox.Item>
136
139
  );
137
140
  };
@@ -9,7 +9,7 @@ import { debounce } from '@dxos/async';
9
9
  import { useClient } from '@dxos/react-client';
10
10
  import { type Identity, useIdentity } from '@dxos/react-client/halo';
11
11
  import { ButtonGroup, Clipboard, Input, useTranslation } from '@dxos/react-ui';
12
- import { Form, type FormFieldMap, type FormUpdateMeta, Settings } from '@dxos/react-ui-form';
12
+ import { Form, type FormFieldMap, type FormUpdateMeta } from '@dxos/react-ui-form';
13
13
  import { EmojiPickerBlock, HuePicker } from '@dxos/react-ui-pickers';
14
14
  import { hexToEmoji, hexToHue } from '@dxos/util';
15
15
 
@@ -101,14 +101,16 @@ export const ProfileContainer = () => {
101
101
  );
102
102
 
103
103
  return (
104
- <Settings.Item title={label} description={t('display-name.description')}>
105
- <Input.TextInput
106
- value={getValue()}
107
- onChange={handleChange}
108
- placeholder={t('display-name-input.placeholder')}
109
- classNames='min-w-64'
110
- />
111
- </Settings.Item>
104
+ <Form.Row label={label} description={t('display-name.description')}>
105
+ <Input.Root>
106
+ <Input.TextInput
107
+ value={getValue()}
108
+ onChange={handleChange}
109
+ placeholder={t('display-name-input.placeholder')}
110
+ classNames='min-w-64'
111
+ />
112
+ </Input.Root>
113
+ </Form.Row>
112
114
  );
113
115
  },
114
116
  emoji: ({ type, label, getValue, onValueChange }) => {
@@ -119,7 +121,7 @@ export const ProfileContainer = () => {
119
121
  );
120
122
 
121
123
  return (
122
- <Settings.Item title={label} description={t('icon.description')}>
124
+ <Form.Row label={label} description={t('icon.description')}>
123
125
  <EmojiPickerBlock
124
126
  triggerVariant='default'
125
127
  emoji={getValue()}
@@ -127,7 +129,7 @@ export const ProfileContainer = () => {
127
129
  onClickClear={handleEmojiReset}
128
130
  classNames='justify-self-end'
129
131
  />
130
- </Settings.Item>
132
+ </Form.Row>
131
133
  );
132
134
  },
133
135
  hue: ({ type, label, getValue, onValueChange }) => {
@@ -138,22 +140,24 @@ export const ProfileContainer = () => {
138
140
  );
139
141
 
140
142
  return (
141
- <Settings.Item title={label} description={t('hue.description')}>
143
+ <Form.Row label={label} description={t('hue.description')}>
142
144
  <div className='flex justify-self-end'>
143
145
  <HuePicker value={getValue()} onChange={handleChange} onReset={handleHueReset} />
144
146
  </div>
145
- </Settings.Item>
147
+ </Form.Row>
146
148
  );
147
149
  },
148
150
  // TODO(wittjosiah): We need text input annotations for disabled and copyable.
149
151
  did: ({ label, getValue }) => {
150
152
  return (
151
- <Settings.Item title={label} description={t('did.description')}>
152
- <ButtonGroup classNames='w-full'>
153
- <Input.TextInput value={getValue()} disabled classNames='min-w-64' />
154
- <Clipboard.IconButton value={getValue() ?? ''} />
155
- </ButtonGroup>
156
- </Settings.Item>
153
+ <Form.Row label={label} description={t('did.description')}>
154
+ <Input.Root>
155
+ <ButtonGroup classNames='w-full'>
156
+ <Input.TextInput value={getValue()} disabled classNames='min-w-64' />
157
+ <Clipboard.IconButton value={getValue() ?? ''} />
158
+ </ButtonGroup>
159
+ </Input.Root>
160
+ </Form.Row>
157
161
  );
158
162
  },
159
163
  }),
@@ -162,15 +166,21 @@ export const ProfileContainer = () => {
162
166
 
163
167
  return (
164
168
  <Clipboard.Provider>
165
- <Settings.Viewport>
166
- <Settings.Section title={t('profile.label')} description={t('profile.description')}>
167
- <Form.Root schema={UserProfile} values={values} fieldMap={fieldMap} onValuesChanged={handleChange}>
168
- <Form.Content classNames='gap-form-gap'>
169
+ <Form.Root
170
+ variant='settings'
171
+ schema={UserProfile}
172
+ values={values}
173
+ fieldMap={fieldMap}
174
+ onValuesChanged={handleChange}
175
+ >
176
+ <Form.Viewport scroll>
177
+ <Form.Content>
178
+ <Form.Section title={t('profile.label')} description={t('profile.description')}>
169
179
  <Form.FieldSet />
170
- </Form.Content>
171
- </Form.Root>
172
- </Settings.Section>
173
- </Settings.Viewport>
180
+ </Form.Section>
181
+ </Form.Content>
182
+ </Form.Viewport>
183
+ </Form.Root>
174
184
  </Clipboard.Provider>
175
185
  );
176
186
  };
@@ -6,8 +6,9 @@ import React from 'react';
6
6
 
7
7
  import { useOperationInvoker } from '@dxos/app-framework/ui';
8
8
  import { useCredentials } from '@dxos/react-client/halo';
9
- import { Icon, IconButton, List, ListItem, Message, useTranslation } from '@dxos/react-ui';
10
- import { Settings } from '@dxos/react-ui-form';
9
+ import { Icon, IconButton, Message, useTranslation } from '@dxos/react-ui';
10
+ import { Form } from '@dxos/react-ui-form';
11
+ import { Listbox } from '@dxos/react-ui-list';
11
12
 
12
13
  import { meta } from '#meta';
13
14
  import { ClientOperation } from '#operations';
@@ -23,44 +24,52 @@ export const RecoveryCredentialsContainer = () => {
23
24
  );
24
25
 
25
26
  return (
26
- <Settings.Viewport>
27
- <Settings.Section title={t('recovery-setup-dialog.title')} description={t('recovery-setup-dialog.description')}>
28
- <Settings.Item title={t('create-passkey.label')} description={t('create-passkey.description')}>
29
- <IconButton
30
- label={t('create-passkey.label')}
31
- icon='ph--key--duotone'
32
- variant='primary'
33
- onClick={() => invokePromise(ClientOperation.CreatePasskey)}
34
- />
35
- </Settings.Item>
36
- <Settings.Item title={t('create-recovery-code.label')} description={t('create-recovery-code.description')}>
37
- <IconButton
38
- label={t('create-recovery-code.label')}
39
- icon='ph--receipt--duotone'
40
- variant='default'
41
- onClick={() => invokePromise(ClientOperation.CreateRecoveryCode)}
42
- />
43
- </Settings.Item>
44
- </Settings.Section>
45
- <Settings.Section title={t('credentials-list.label')}>
46
- {recoveryCredentials.length < 1 ? (
47
- <Message.Root valence='error'>
48
- <Message.Title icon='ph--shield-warning--duotone'>{t('no-credentials.title')}</Message.Title>
49
- <Message.Content>{t('no-credentials.message')}</Message.Content>
50
- </Message.Root>
51
- ) : (
52
- <List>
53
- {recoveryCredentials.map((credential) => (
54
- <ListItem.Root key={credential.id?.toHex()}>
55
- <ListItem.Endcap>
56
- <Icon icon='ph--key--regular' />
57
- </ListItem.Endcap>
58
- <ListItem.Heading>{credential.issuanceDate.toLocaleString()}</ListItem.Heading>
59
- </ListItem.Root>
60
- ))}
61
- </List>
62
- )}
63
- </Settings.Section>
64
- </Settings.Viewport>
27
+ <Form.Root variant='settings'>
28
+ <Form.Viewport scroll>
29
+ <Form.Content>
30
+ <Form.Section title={t('recovery-setup-dialog.title')} description={t('recovery-setup-dialog.description')}>
31
+ <Form.Row label={t('create-passkey.label')} description={t('create-passkey.description')}>
32
+ <IconButton
33
+ label={t('create-passkey.label')}
34
+ icon='ph--key--duotone'
35
+ variant='primary'
36
+ onClick={() => invokePromise(ClientOperation.CreatePasskey)}
37
+ />
38
+ </Form.Row>
39
+ <Form.Row label={t('create-recovery-code.label')} description={t('create-recovery-code.description')}>
40
+ <IconButton
41
+ label={t('create-recovery-code.label')}
42
+ icon='ph--receipt--duotone'
43
+ variant='default'
44
+ onClick={() => invokePromise(ClientOperation.CreateRecoveryCode)}
45
+ />
46
+ </Form.Row>
47
+ </Form.Section>
48
+ <Form.Section title={t('credentials-list.label')}>
49
+ {recoveryCredentials.length < 1 ? (
50
+ <Message.Root valence='error'>
51
+ <Message.Title icon='ph--shield-warning--duotone'>{t('no-credentials.title')}</Message.Title>
52
+ <Message.Content>{t('no-credentials.message')}</Message.Content>
53
+ </Message.Root>
54
+ ) : (
55
+ <Listbox.Root>
56
+ <Listbox.Content classNames='gap-1'>
57
+ {recoveryCredentials.map((credential, index) => (
58
+ <Listbox.Item
59
+ key={credential.id?.toHex() ?? index}
60
+ id={credential.id?.toHex() ?? `${index}`}
61
+ classNames='gap-2'
62
+ >
63
+ <Icon icon='ph--key--regular' />
64
+ <Listbox.ItemLabel>{credential.issuanceDate.toLocaleString()}</Listbox.ItemLabel>
65
+ </Listbox.Item>
66
+ ))}
67
+ </Listbox.Content>
68
+ </Listbox.Root>
69
+ )}
70
+ </Form.Section>
71
+ </Form.Content>
72
+ </Form.Viewport>
73
+ </Form.Root>
65
74
  );
66
75
  };
@@ -9,39 +9,40 @@ import { log } from '@dxos/log';
9
9
  import { type GetProfileUsageResponse } from '@dxos/protocols';
10
10
  import { useAsyncEffect } from '@dxos/react-ui';
11
11
 
12
- import { useHubHttpClient } from '../../state/use-hub-http';
12
+ import { useHubHttpClient } from '../../hooks';
13
13
  import { UsageView, type UsageViewState } from './UsageView';
14
14
 
15
+ /** State + payload kept together so `ready` always carries data (mirrors the discriminated `UsageViewProps`). */
16
+ type UsageFetchState = { state: Exclude<UsageViewState, 'ready'> } | { state: 'ready'; data: GetProfileUsageResponse };
17
+
15
18
  /**
16
19
  * Fetches rolling-window profile usage from the hub service and renders it.
17
20
  * Connected wrapper around the presentational {@link UsageView}.
18
21
  */
19
22
  export const UsageContainer = () => {
20
23
  const hubHttp = useHubHttpClient();
21
- const [state, setState] = useState<UsageViewState>('loading');
22
- const [data, setData] = useState<GetProfileUsageResponse | undefined>();
24
+ const [fetchState, setFetchState] = useState<UsageFetchState>({ state: 'loading' });
23
25
  const [lastUpdated, setLastUpdated] = useState<number | undefined>();
24
26
  const [refreshCount, setRefreshCount] = useState(0);
25
27
 
26
28
  useAsyncEffect(async () => {
27
29
  if (!hubHttp) {
28
- setState('unavailable');
30
+ setFetchState({ state: 'unavailable' });
29
31
  return;
30
32
  }
31
- // Keep prior data visible while refetching; only show the skeleton on first load.
32
- setState((previous) => (previous === 'ready' ? previous : 'loading'));
33
+ // Keep prior data visible while refetching; only show the loading state on first load.
34
+ setFetchState((previous) => (previous.state === 'ready' ? previous : { state: 'loading' }));
33
35
  try {
34
36
  const result = await hubHttp.getProfileUsage(new Context());
35
- setData(result);
37
+ setFetchState({ state: 'ready', data: result });
36
38
  setLastUpdated(Date.now());
37
- setState('ready');
38
39
  } catch (err) {
39
40
  log.catch(err);
40
- setState('error');
41
+ setFetchState({ state: 'error' });
41
42
  }
42
43
  }, [hubHttp, refreshCount]);
43
44
 
44
45
  const handleRefresh = useCallback(() => setRefreshCount((count) => count + 1), []);
45
46
 
46
- return <UsageView state={state} data={data} lastUpdated={lastUpdated} onRefresh={handleRefresh} />;
47
+ return <UsageView {...fetchState} lastUpdated={lastUpdated} onRefresh={handleRefresh} />;
47
48
  };