@dxos/react-ui 0.1.24 → 0.1.25-next.3c6b542

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 (132) hide show
  1. package/dist/lib/browser/index.mjs +1092 -452
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/IdentityList/DeviceList.d.ts +7 -0
  5. package/dist/types/src/components/IdentityList/DeviceList.d.ts.map +1 -0
  6. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts +2 -1
  7. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts.map +1 -1
  8. package/dist/types/src/components/IdentityList/IdentityListItem.stories.d.ts +2 -1
  9. package/dist/types/src/components/IdentityList/IdentityListItem.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts +3 -2
  11. package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts.map +1 -1
  12. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts +3 -1
  13. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts.map +1 -1
  14. package/dist/types/src/components/IdentityList/index.d.ts +1 -0
  15. package/dist/types/src/components/IdentityList/index.d.ts.map +1 -1
  16. package/dist/types/src/components/InvitationList/InvitationList.d.ts.map +1 -1
  17. package/dist/types/src/components/InvitationList/InvitationListItem.d.ts.map +1 -1
  18. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts +6 -1
  19. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts.map +1 -1
  20. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts +1 -1
  21. package/dist/types/src/components/SpaceList/SpaceListItem.d.ts +7 -0
  22. package/dist/types/src/components/SpaceList/SpaceListItem.d.ts.map +1 -0
  23. package/dist/types/src/components/SpaceList/index.d.ts +2 -0
  24. package/dist/types/src/components/SpaceList/index.d.ts.map +1 -0
  25. package/dist/types/src/components/index.d.ts +1 -0
  26. package/dist/types/src/components/index.d.ts.map +1 -1
  27. package/dist/types/src/composites/DevicesDialog/DevicesDialog.d.ts +6 -0
  28. package/dist/types/src/composites/DevicesDialog/DevicesDialog.d.ts.map +1 -0
  29. package/dist/types/src/composites/DevicesDialog/index.d.ts +2 -0
  30. package/dist/types/src/composites/DevicesDialog/index.d.ts.map +1 -0
  31. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts +1 -1
  32. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts.map +1 -1
  33. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts +2 -2
  34. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -1
  35. package/dist/types/src/composites/Shell/Shell.d.ts +6 -0
  36. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -0
  37. package/dist/types/src/composites/Shell/index.d.ts +2 -0
  38. package/dist/types/src/composites/Shell/index.d.ts.map +1 -0
  39. package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts +6 -0
  40. package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts.map +1 -0
  41. package/dist/types/src/composites/SpaceDialog/index.d.ts +2 -0
  42. package/dist/types/src/composites/SpaceDialog/index.d.ts.map +1 -0
  43. package/dist/types/src/composites/index.d.ts +2 -0
  44. package/dist/types/src/composites/index.d.ts.map +1 -1
  45. package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts.map +1 -1
  46. package/dist/types/src/layouts/PanelDialog/PanelDialog.d.ts +14 -0
  47. package/dist/types/src/layouts/PanelDialog/PanelDialog.d.ts.map +1 -0
  48. package/dist/types/src/layouts/PanelDialog/index.d.ts +2 -0
  49. package/dist/types/src/layouts/PanelDialog/index.d.ts.map +1 -0
  50. package/dist/types/src/layouts/index.d.ts +1 -0
  51. package/dist/types/src/layouts/index.d.ts.map +1 -1
  52. package/dist/types/src/panels/DevicesPanel/DevicesPanel.d.ts +10 -0
  53. package/dist/types/src/panels/DevicesPanel/DevicesPanel.d.ts.map +1 -0
  54. package/dist/types/src/panels/DevicesPanel/index.d.ts +2 -0
  55. package/dist/types/src/panels/DevicesPanel/index.d.ts.map +1 -0
  56. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +3 -0
  57. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -1
  58. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +1 -1
  59. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -1
  60. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +4 -3
  61. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -1
  62. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +1 -1
  63. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -1
  64. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts +10 -2
  65. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts.map +1 -1
  66. package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts.map +1 -1
  67. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +1 -1
  68. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +1 -1
  69. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -1
  70. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -1
  71. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -1
  72. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -1
  73. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts +18 -0
  74. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -0
  75. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts +9 -0
  76. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -0
  77. package/dist/types/src/panels/SpacePanel/index.d.ts +2 -0
  78. package/dist/types/src/panels/SpacePanel/index.d.ts.map +1 -0
  79. package/dist/types/src/panels/index.d.ts +2 -0
  80. package/dist/types/src/panels/index.d.ts.map +1 -1
  81. package/dist/types/src/styles/dialogStyles.d.ts +3 -0
  82. package/dist/types/src/styles/dialogStyles.d.ts.map +1 -0
  83. package/dist/types/src/styles/index.d.ts +1 -0
  84. package/dist/types/src/styles/index.d.ts.map +1 -1
  85. package/dist/types/src/translations/locales/en-US.d.ts +12 -1
  86. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  87. package/package.json +12 -11
  88. package/src/components/IdentityList/DeviceList.tsx +31 -0
  89. package/src/components/IdentityList/IdentityListItem.tsx +15 -3
  90. package/src/components/IdentityList/SpaceMemberList.tsx +10 -8
  91. package/src/components/IdentityList/SpaceMemberListContainer.tsx +5 -4
  92. package/src/components/IdentityList/index.ts +1 -0
  93. package/src/components/InvitationList/InvitationList.tsx +6 -2
  94. package/src/components/InvitationList/InvitationListItem.tsx +36 -25
  95. package/src/components/InvitationList/InvitationStatusAvatar.tsx +10 -5
  96. package/src/components/SpaceList/SpaceListItem.tsx +32 -0
  97. package/src/components/SpaceList/index.ts +5 -0
  98. package/src/components/index.ts +1 -0
  99. package/src/composites/DevicesDialog/DevicesDialog.tsx +38 -0
  100. package/src/composites/DevicesDialog/index.ts +5 -0
  101. package/src/composites/JoinDialog/JoinDialog.stories.tsx +59 -58
  102. package/src/composites/JoinDialog/JoinDialog.tsx +3 -1
  103. package/src/composites/Shell/Shell.tsx +92 -0
  104. package/src/composites/Shell/index.ts +5 -0
  105. package/src/composites/SpaceDialog/SpaceDialog.tsx +38 -0
  106. package/src/composites/SpaceDialog/index.ts +5 -0
  107. package/src/composites/index.ts +3 -0
  108. package/src/layouts/PanelAlertDialog/PanelAlertDialog.tsx +4 -8
  109. package/src/layouts/PanelDialog/PanelDialog.tsx +39 -0
  110. package/src/layouts/PanelDialog/index.ts +5 -0
  111. package/src/layouts/PanelSidebar/PanelSidebar.tsx +1 -1
  112. package/src/layouts/index.ts +1 -0
  113. package/src/panels/DevicesPanel/DevicesPanel.tsx +98 -0
  114. package/src/panels/DevicesPanel/index.ts +5 -0
  115. package/src/panels/JoinPanel/JoinHeading.tsx +44 -16
  116. package/src/panels/JoinPanel/JoinPanel.tsx +32 -11
  117. package/src/panels/JoinPanel/JoinPanelProps.ts +4 -8
  118. package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +5 -1
  119. package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +63 -24
  120. package/src/panels/JoinPanel/view-states/IdentityInput.tsx +8 -1
  121. package/src/panels/JoinPanel/view-states/IdentitySelector.tsx +4 -1
  122. package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +1 -0
  123. package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +4 -1
  124. package/src/panels/JoinPanel/view-states/InvitationInput.tsx +8 -1
  125. package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +4 -1
  126. package/src/panels/SpacePanel/SpacePanel.stories.tsx +53 -0
  127. package/src/panels/SpacePanel/SpacePanel.tsx +174 -0
  128. package/src/panels/SpacePanel/index.ts +5 -0
  129. package/src/panels/index.ts +2 -0
  130. package/src/styles/dialogStyles.ts +8 -0
  131. package/src/styles/index.ts +1 -0
  132. package/src/translations/locales/en-US.ts +14 -3
@@ -0,0 +1,98 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { UserPlus, X } from 'phosphor-react';
6
+ import React, { cloneElement, useReducer } from 'react';
7
+
8
+ import { useClient, useDevices, useHaloInvitations, useIdentity } from '@dxos/react-client';
9
+ import { Button, getSize, mx, Tooltip, useTranslation } from '@dxos/react-components';
10
+
11
+ import { DeviceList, InvitationList, PanelSeparator } from '../../components';
12
+ import { defaultSurface, subduedSurface } from '../../styles';
13
+
14
+ export type DevicesPanelProps = {
15
+ titleId?: string;
16
+ createInvitationUrl: (invitationCode: string) => string;
17
+ doneActionParent?: Parameters<typeof cloneElement>[0];
18
+ onDone?: () => void;
19
+ };
20
+
21
+ export type DevicesView = 'device list';
22
+
23
+ const DeviceListView = ({ createInvitationUrl, titleId, onDone, doneActionParent }: DevicesPanelProps) => {
24
+ const { t } = useTranslation('os');
25
+ const client = useClient();
26
+ const identity = useIdentity();
27
+ const devices = useDevices();
28
+ const invitations = useHaloInvitations();
29
+ const displayName = identity?.displayName;
30
+
31
+ if (!identity) {
32
+ return null;
33
+ }
34
+
35
+ const doneButton = (
36
+ <Button compact variant='ghost' onClick={() => onDone?.()} data-testid='show-all-spaces'>
37
+ <X className={getSize(4)} weight='bold' />
38
+ </Button>
39
+ );
40
+
41
+ return (
42
+ <div role='none' className='flex flex-col'>
43
+ <div role='none' className={mx(subduedSurface, 'rounded-bs-md flex items-center p-2 gap-2')}>
44
+ {/* TODO(wittjosiah): Label this as the identity panel. */}
45
+ <h2 id={titleId} className={mx('grow font-system-medium', !displayName && 'font-mono')}>
46
+ {displayName ?? identity.identityKey.truncate()}
47
+ </h2>
48
+ <Tooltip content={t('close label')} zIndex='z-50'>
49
+ {doneActionParent ? cloneElement(doneActionParent, {}, doneButton) : doneButton}
50
+ </Tooltip>
51
+ </div>
52
+ <div role='region' className={mx(defaultSurface, 'rounded-be-md p-2')}>
53
+ <InvitationList
54
+ invitations={invitations}
55
+ onClickRemove={({ invitation }) => invitation && client.halo.removeInvitation(invitation.invitationId!)}
56
+ createInvitationUrl={createInvitationUrl}
57
+ />
58
+ <Button
59
+ className='is-full flex gap-2 mbs-2'
60
+ compact
61
+ onClick={() => client.halo.createInvitation()}
62
+ data-testid='create-space-invitation'
63
+ >
64
+ <span>{t('create space invitation label')}</span>
65
+ <UserPlus className={getSize(4)} weight='bold' />
66
+ </Button>
67
+ <PanelSeparator />
68
+ <DeviceList devices={devices} />
69
+ </div>
70
+ </div>
71
+ );
72
+ };
73
+
74
+ type DevicesPanelState = {
75
+ activeView: DevicesView;
76
+ };
77
+
78
+ type DevicesPanelAction = {
79
+ type: null;
80
+ };
81
+
82
+ // TODO(wittjosiah): Rename identity panel and include other views?
83
+ export const DevicesPanel = (props: DevicesPanelProps) => {
84
+ const reducer = (state: DevicesPanelState, action: DevicesPanelAction) => {
85
+ const nextState = { ...state };
86
+ switch (action.type) {
87
+ case null:
88
+ }
89
+ return nextState;
90
+ };
91
+
92
+ const [panelState] = useReducer(reducer, {
93
+ activeView: 'device list'
94
+ });
95
+
96
+ // TODO(wittjosiah): Use ViewState or similar.
97
+ return <>{panelState.activeView === 'device list' ? <DeviceListView {...props} /> : null}</>;
98
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './DevicesPanel';
@@ -9,25 +9,41 @@ import { useSpace } from '@dxos/react-client';
9
9
  import { Avatar, Button, getSize, Heading, mx, Trans, useId, useTranslation } from '@dxos/react-components';
10
10
 
11
11
  import { subduedSurface } from '../../styles';
12
+ import { JoinPanelMode } from './JoinPanelProps';
12
13
 
13
14
  export interface JoinSpaceHeadingProps {
15
+ mode?: JoinPanelMode;
14
16
  titleId: string;
15
17
  invitation?: AuthenticatingInvitationObservable;
16
18
  onExit?: () => void;
17
19
  exitActionParent?: Parameters<typeof cloneElement>[0];
20
+ preventExit?: boolean;
18
21
  }
19
22
 
20
23
  export const JoinHeading = forwardRef(
21
- ({ titleId, invitation, onExit, exitActionParent }: JoinSpaceHeadingProps, ref: ForwardedRef<HTMLDivElement>) => {
24
+ (
25
+ { mode, titleId, invitation, onExit, exitActionParent, preventExit }: JoinSpaceHeadingProps,
26
+ ref: ForwardedRef<HTMLDivElement>
27
+ ) => {
22
28
  const { t } = useTranslation('os');
23
29
 
24
30
  const space = useSpace(invitation?.invitation?.spaceKey);
25
- const spaceTitle = space?.properties.name ?? '(Space not available)';
31
+ const name = mode === 'halo-only' ? '(Unknown identity)' : space?.properties.name ?? '(Space name not available)';
32
+ const nameId = useId(mode === 'halo-only' ? 'identityDisplayName' : 'spaceDisplayName');
26
33
 
27
- const nameId = useId('spaceDisplayName');
34
+ const invitationKey =
35
+ mode === 'halo-only'
36
+ ? invitation?.invitation?.identityKey?.toHex()
37
+ : invitation?.invitation?.identityKey?.toHex();
28
38
 
29
39
  const exitButton = (
30
- <Button compact variant='ghost' {...(onExit && { onClick: onExit })} className='grow-0 shrink-0'>
40
+ <Button
41
+ compact
42
+ variant='ghost'
43
+ {...(onExit && { onClick: onExit })}
44
+ className='grow-0 shrink-0'
45
+ data-testid='join-exit'
46
+ >
31
47
  <ProhibitInset className={getSize(5)} />
32
48
  <span className='sr-only'>{t('exit label')}</span>
33
49
  </Button>
@@ -36,20 +52,32 @@ export const JoinHeading = forwardRef(
36
52
  return (
37
53
  <div role='none' className={mx(subduedSurface, 'p-2 rounded-bs-md')} ref={ref}>
38
54
  <div role='group' className='flex items-center gap-2'>
39
- <Avatar fallbackValue={invitation?.invitation?.spaceKey?.toHex() ?? ''} labelId={nameId} />
55
+ {invitationKey ? (
56
+ <Avatar fallbackValue={invitationKey} labelId={nameId} />
57
+ ) : (
58
+ <span role='none' className={mx(getSize(10), 'bg-neutral-300 dark:bg-neutral-700 rounded-full')} />
59
+ )}
40
60
  <Heading level={1} className='font-body font-normal text-base grow' id={titleId}>
41
- <Trans
42
- {...{
43
- defaults: t('join space heading'),
44
- components: {
45
- small: <span className='block leading-none mbe-1 font-system-medium text-sm' />,
46
- large: <span className='block leading-none' id={nameId} />
47
- },
48
- values: { spaceTitle }
49
- }}
50
- />
61
+ {invitation ? (
62
+ <Trans
63
+ {...{
64
+ defaults: t('joining heading'),
65
+ components: {
66
+ small: <span className='block leading-none mbe-1 font-system-medium text-sm' />,
67
+ large: <span className='block leading-none' id={nameId} />
68
+ },
69
+ values: { name }
70
+ }}
71
+ />
72
+ ) : (
73
+ <span className='block leading-none font-system-medium text-sm'>
74
+ {mode === 'halo-only' ? t('halo heading') : t('join space heading')}
75
+ </span>
76
+ )}
51
77
  </Heading>
52
- {exitActionParent ? cloneElement(exitActionParent, {}, exitButton) : exitButton}
78
+ {!preventExit &&
79
+ mode !== 'halo-only' &&
80
+ (exitActionParent ? cloneElement(exitActionParent, {}, exitButton) : exitButton)}
53
81
  </div>
54
82
  </div>
55
83
  );
@@ -1,7 +1,7 @@
1
1
  //
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
- import React, { useEffect, useReducer } from 'react';
4
+ import React, { useEffect, useReducer, useState } from 'react';
5
5
 
6
6
  import { InvitationEncoder } from '@dxos/client';
7
7
  import { log } from '@dxos/log';
@@ -22,18 +22,20 @@ import {
22
22
  } from './view-states';
23
23
 
24
24
  export const JoinPanel = ({
25
+ mode,
25
26
  initialInvitationCode,
26
27
  titleId: propsTitleId,
27
28
  exitActionParent,
28
29
  onExit,
29
30
  doneActionParent,
30
- onDone
31
+ onDone,
32
+ preventExit
31
33
  }: JoinPanelProps) => {
32
34
  const client = useClient();
33
35
  const internalTitleId = useId('joinPanel__title');
34
36
  const titleId = propsTitleId ?? internalTitleId;
35
37
  const identity = useIdentity();
36
-
38
+ const [prevIdentity, setPrevIdentity] = useState(identity);
37
39
  const availableIdentities = identity ? [identity] : [];
38
40
 
39
41
  const reducer = (state: JoinState, action: JoinAction) => {
@@ -50,7 +52,7 @@ export const JoinPanel = ({
50
52
  if (action.method === 'accept device invitation') {
51
53
  nextState.activeView = 'halo invitation acceptor';
52
54
  if (state.unredeemedHaloInvitationCode) {
53
- nextState.haloInvitation = client.echo.acceptInvitation(
55
+ nextState.haloInvitation = client.halo.acceptInvitation(
54
56
  InvitationEncoder.decode(state.unredeemedHaloInvitationCode)
55
57
  );
56
58
  nextState.unredeemedHaloInvitationCode = undefined;
@@ -120,13 +122,14 @@ export const JoinPanel = ({
120
122
  nextState[action.from === 'halo' ? 'haloViewState' : 'spaceViewState'] = 'invitation input';
121
123
  break;
122
124
  }
123
- log.debug('[join panel reducer]', { action, nextState });
125
+ log('[join panel reducer]', { action, nextState });
124
126
  return nextState;
125
127
  };
126
128
 
129
+ const unredeemedHaloInvitationCode = mode === 'halo-only' ? initialInvitationCode : undefined;
127
130
  const [joinState, dispatch] = useReducer(reducer, {
128
- unredeemedHaloInvitationCode: undefined,
129
- unredeemedSpaceInvitationCode: initialInvitationCode,
131
+ unredeemedHaloInvitationCode,
132
+ unredeemedSpaceInvitationCode: mode === 'halo-only' ? undefined : initialInvitationCode,
130
133
  spaceInvitation: undefined,
131
134
  haloInvitation: undefined,
132
135
  activeView: availableIdentities.length > 0 ? 'identity selector' : 'addition method selector',
@@ -136,6 +139,17 @@ export const JoinPanel = ({
136
139
  haloViewState: 'invitation input'
137
140
  });
138
141
 
142
+ if (identity !== prevIdentity) {
143
+ setPrevIdentity(identity);
144
+ identity && !prevIdentity && dispatch({ type: 'added identity', identity });
145
+ }
146
+
147
+ useEffect(() => {
148
+ if (unredeemedHaloInvitationCode) {
149
+ dispatch({ type: 'select addition method', method: 'accept device invitation' });
150
+ }
151
+ }, [unredeemedHaloInvitationCode]);
152
+
139
153
  useEffect(() => {
140
154
  // TODO (thure): Validate if this is sufficiently synchronous for iOS to move focus. It might not be!
141
155
  const attrValue =
@@ -146,7 +160,7 @@ export const JoinPanel = ({
146
160
  : joinState.activeView === 'halo invitation acceptor'
147
161
  ? `${joinState.activeView}; ${joinState.haloViewState}`
148
162
  : joinState.activeView;
149
- log.debug('[autofocus value]', { attrValue });
163
+ log('[autofocus value]', { attrValue });
150
164
  const $nextAutofocus: HTMLElement | null = document.querySelector(`[data-autofocus="${attrValue}"]`);
151
165
  if ($nextAutofocus) {
152
166
  $nextAutofocus.focus();
@@ -177,7 +191,9 @@ export const JoinPanel = ({
177
191
 
178
192
  return (
179
193
  <>
180
- <JoinHeading {...{ titleId, invitation: joinState.spaceInvitation, onExit, exitActionParent }} />
194
+ <JoinHeading
195
+ {...{ mode, titleId, invitation: joinState.spaceInvitation, onExit, exitActionParent, preventExit }}
196
+ />
181
197
  <div role='none' className='is-full overflow-hidden'>
182
198
  <div role='none' className='flex is-[1300%]' aria-live='polite'>
183
199
  <IdentitySelector
@@ -240,14 +256,19 @@ export const JoinPanel = ({
240
256
  active:
241
257
  joinState.activeView === 'halo invitation acceptor' &&
242
258
  joinState.haloViewState === 'invitation accepted',
243
- invitationType: 'halo'
259
+ invitationType: 'halo',
260
+ doneActionParent,
261
+ onDone
244
262
  }}
245
263
  />
246
264
  <IdentityAdded
247
265
  {...{
266
+ mode,
248
267
  dispatch,
249
268
  addedIdentity: joinState.selectedIdentity,
250
- active: joinState.activeView === 'identity added'
269
+ active: joinState.activeView === 'identity added',
270
+ doneActionParent,
271
+ onDone
251
272
  }}
252
273
  />
253
274
  <InvitationInput
@@ -4,22 +4,18 @@
4
4
 
5
5
  import { cloneElement, Dispatch } from 'react';
6
6
 
7
- import type {
8
- AuthenticatingInvitationObservable,
9
- Profile as NaturalProfile,
10
- Space as NaturalSpace
11
- } from '@dxos/client';
7
+ import type { AuthenticatingInvitationObservable, Profile } from '@dxos/client';
12
8
  import { InvitationResult } from '@dxos/react-client';
13
9
 
14
- export type Space = Pick<NaturalSpace, 'properties'>;
15
-
16
- export type Profile = Pick<NaturalProfile, 'displayName' | 'identityKey'>;
10
+ export type JoinPanelMode = 'default' | 'halo-only';
17
11
 
18
12
  export interface JoinPanelProps {
13
+ mode?: JoinPanelMode;
19
14
  initialInvitationCode?: string;
20
15
  titleId?: string;
21
16
  exitActionParent?: Parameters<typeof cloneElement>[0];
22
17
  onExit?: () => void;
18
+ preventExit?: boolean;
23
19
  doneActionParent?: Parameters<typeof cloneElement>[0];
24
20
  onDone?: (result: InvitationResult | null) => void;
25
21
  parseInvitationCodeInput?: (invitationCodeInput: string) => string;
@@ -5,9 +5,9 @@
5
5
  import { CaretLeft, CaretRight, Plus, QrCode, Textbox } from 'phosphor-react';
6
6
  import React from 'react';
7
7
 
8
+ import type { Profile } from '@dxos/client';
8
9
  import { Button, CompoundButton, getSize, mx, useTranslation } from '@dxos/react-components';
9
10
 
10
- import { Profile } from '../JoinPanelProps';
11
11
  import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
12
 
13
13
  export interface AdditionMethodSelectorProps extends ViewStateProps {
@@ -36,6 +36,7 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
36
36
  before={<Plus className={getSize(6)} />}
37
37
  onClick={() => dispatch({ type: 'select addition method', method: 'create identity' })}
38
38
  data-autofocus='addition method selector'
39
+ data-testid='create-identity'
39
40
  >
40
41
  {t('create identity label')}
41
42
  </CompoundButton>
@@ -44,6 +45,7 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
44
45
  description={t('join identity description')}
45
46
  before={<QrCode className={getSize(6)} />}
46
47
  onClick={() => dispatch({ type: 'select addition method', method: 'accept device invitation' })}
48
+ data-testid='join-identity'
47
49
  >
48
50
  {t('join identity label')}
49
51
  </CompoundButton>
@@ -52,6 +54,7 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
52
54
  description={t('recover identity description')}
53
55
  before={<Textbox className={getSize(6)} />}
54
56
  onClick={() => dispatch({ type: 'select addition method', method: 'recover identity' })}
57
+ data-testid='recover-identity'
55
58
  >
56
59
  {t('recover identity label')}
57
60
  </CompoundButton>
@@ -59,6 +62,7 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
59
62
  <Button
60
63
  disabled={disabled || availableIdentities.length < 1}
61
64
  onClick={() => dispatch({ type: 'deselect identity' })}
65
+ data-testid='deselect-identity'
62
66
  >
63
67
  <CaretLeft className={getSize(4)} weight='bold' />
64
68
  <span className='grow'>{t('deselect identity label')}</span>
@@ -2,19 +2,54 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CaretLeft, CaretRight } from 'phosphor-react';
6
- import React from 'react';
5
+ import { CaretLeft, CaretRight, Check } from 'phosphor-react';
6
+ import React, { cloneElement } from 'react';
7
7
 
8
8
  import type { Profile } from '@dxos/client';
9
+ import { InvitationResult } from '@dxos/react-client';
9
10
  import { Avatar, Button, getSize, mx, useTranslation } from '@dxos/react-components';
10
11
 
12
+ import { JoinPanelMode } from '../JoinPanelProps';
11
13
  import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
14
 
13
- export interface IdentityAddedProps extends ViewStateProps {
15
+ export interface IdentityAddedProps extends ViewStateProps, DoneProps {
16
+ mode?: JoinPanelMode;
14
17
  addedIdentity?: Profile;
15
18
  }
16
19
 
17
- export const IdentityAdded = ({ addedIdentity, ...viewStateProps }: IdentityAddedProps) => {
20
+ export interface DoneProps extends ViewStateProps {
21
+ doneActionParent?: Parameters<typeof cloneElement>[0];
22
+ onDone?: (result: InvitationResult | null) => void;
23
+ }
24
+
25
+ const Done = ({ onDone, doneActionParent, active }: DoneProps) => {
26
+ const disabled = !active;
27
+ const { t } = useTranslation('os');
28
+
29
+ const doneButton = (
30
+ <Button
31
+ {...(onDone && { onClick: () => onDone(null) })}
32
+ disabled={disabled}
33
+ className='grow flex items-center gap-2 pli-2'
34
+ data-autofocus='identity added'
35
+ data-testid='identity-added-done'
36
+ >
37
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
38
+ <span className='grow'>{t('done label')}</span>
39
+ <Check weight='bold' className={getSize(4)} />
40
+ </Button>
41
+ );
42
+
43
+ return doneActionParent ? cloneElement(doneActionParent, {}, doneButton) : doneButton;
44
+ };
45
+
46
+ export const IdentityAdded = ({
47
+ mode,
48
+ addedIdentity,
49
+ onDone,
50
+ doneActionParent,
51
+ ...viewStateProps
52
+ }: IdentityAddedProps) => {
18
53
  const disabled = !viewStateProps.active;
19
54
  const { dispatch } = viewStateProps;
20
55
  const { t } = useTranslation('os');
@@ -35,26 +70,30 @@ export const IdentityAdded = ({ addedIdentity, ...viewStateProps }: IdentityAdde
35
70
  status='active'
36
71
  />
37
72
  </div>
38
- <div className='flex gap-2'>
39
- <Button
40
- disabled={disabled || !addedIdentity}
41
- className='grow flex items-center gap-2 pli-2 order-2'
42
- onClick={() => addedIdentity && dispatch({ type: 'select identity', identity: addedIdentity })}
43
- data-autofocus='identity added'
44
- >
45
- <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
46
- <span className='grow'>{t('continue label')}</span>
47
- <CaretRight weight='bold' className={getSize(4)} />
48
- </Button>
49
- <Button
50
- disabled={disabled}
51
- onClick={() => dispatch({ type: 'deselect identity' })}
52
- className='flex items-center gap-2 pis-2 pie-4'
53
- >
54
- <CaretLeft weight='bold' className={getSize(4)} />
55
- <span>{t('deselect identity label')}</span>
56
- </Button>
57
- </div>
73
+ {mode === 'halo-only' ? (
74
+ <Done onDone={onDone} doneActionParent={doneActionParent} {...viewStateProps} />
75
+ ) : (
76
+ <div className='flex gap-2'>
77
+ <Button
78
+ disabled={disabled || !addedIdentity}
79
+ className='grow flex items-center gap-2 pli-2 order-2'
80
+ onClick={() => addedIdentity && dispatch({ type: 'select identity', identity: addedIdentity })}
81
+ data-autofocus='identity added'
82
+ >
83
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
84
+ <span className='grow'>{t('continue label')}</span>
85
+ <CaretRight weight='bold' className={getSize(4)} />
86
+ </Button>
87
+ <Button
88
+ disabled={disabled}
89
+ onClick={() => dispatch({ type: 'deselect identity' })}
90
+ className='flex items-center gap-2 pis-2 pie-4'
91
+ >
92
+ <CaretLeft weight='bold' className={getSize(4)} />
93
+ <span>{t('deselect identity label')}</span>
94
+ </Button>
95
+ </div>
96
+ )}
58
97
  </ViewState>
59
98
  );
60
99
  };
@@ -50,10 +50,16 @@ export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProp
50
50
  } as ComponentPropsWithoutRef<'input'>
51
51
  }}
52
52
  {...(validationMessage.length && { validationValence: 'error', validationMessage })}
53
+ data-testid='identity-input'
53
54
  />
54
55
  <div role='none' className='grow' />
55
56
  <div className='flex gap-2'>
56
- <Button disabled={disabled} className='grow flex items-center gap-2 pli-2 order-2' onClick={handleNext}>
57
+ <Button
58
+ disabled={disabled}
59
+ className='grow flex items-center gap-2 pli-2 order-2'
60
+ onClick={handleNext}
61
+ data-testid={`${method === 'recover identity' ? 'recover' : 'create'}-identity-input-continue`}
62
+ >
57
63
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
58
64
  <span className='grow'>{t('continue label')}</span>
59
65
  <CaretRight weight='bold' className={getSize(4)} />
@@ -62,6 +68,7 @@ export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProp
62
68
  disabled={disabled}
63
69
  onClick={() => dispatch({ type: 'add identity' })}
64
70
  className='flex items-center gap-2 pis-2 pie-4'
71
+ data-testid={`${method === 'recover identity' ? 'recover' : 'create'}-identity-input-back`}
65
72
  >
66
73
  <CaretLeft weight='bold' className={getSize(4)} />
67
74
  <span>{t('back label')}</span>
@@ -6,9 +6,9 @@ import * as RadioGroup from '@radix-ui/react-radio-group';
6
6
  import { CaretLeft, CaretRight, UserPlus } from 'phosphor-react';
7
7
  import React, { useCallback, useState } from 'react';
8
8
 
9
+ import type { Profile } from '@dxos/client';
9
10
  import { Avatar, Button, themeVariantFocus, getSize, mx, useTranslation } from '@dxos/react-components';
10
11
 
11
- import { Profile } from '../JoinPanelProps';
12
12
  import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
13
13
 
14
14
  export interface IdentitySelectorProps extends ViewStateProps {
@@ -57,6 +57,7 @@ export const IdentitySelector = ({ availableIdentities, ...viewStateProps }: Ide
57
57
  'radix-state-unchecked:bg-neutral-100 dark:radix-state-unchecked:bg-neutral-900',
58
58
  themeVariantFocus('os')
59
59
  )}
60
+ data-testid='identity-selector-item'
60
61
  >
61
62
  <RadioGroup.Indicator className='absolute inset-0 flex items-center justify-center leading-0'>
62
63
  <div className='w-1.5 h-1.5 rounded-full bg-white' />
@@ -75,6 +76,7 @@ export const IdentitySelector = ({ availableIdentities, ...viewStateProps }: Ide
75
76
  compact
76
77
  onClick={() => dispatch({ type: 'add identity' })}
77
78
  className='flex items-center gap-2 pli-2'
79
+ data-testid='add-identity'
78
80
  >
79
81
  <span>{t('add identity label')}</span>
80
82
  <UserPlus weight='bold' className={getSize(3.5)} />
@@ -88,6 +90,7 @@ export const IdentitySelector = ({ availableIdentities, ...viewStateProps }: Ide
88
90
  identity: activeIdentity
89
91
  })
90
92
  }
93
+ data-testid='select-identity'
91
94
  >
92
95
  <CaretLeft weight='bold' className={mx(getSize(4), 'invisible')} />
93
96
  <span className='grow'>{t('continue label')}</span>
@@ -33,6 +33,7 @@ const PureInvitationAcceptedContent = ({
33
33
  disabled={disabled}
34
34
  className='grow flex items-center gap-2 pli-2'
35
35
  data-autofocus={`${invitationType} invitation acceptor; invitation accepted`}
36
+ data-testid={`${invitationType}-invitation-accepted-done`}
36
37
  >
37
38
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
38
39
  <span className='grow'>{t('done label')}</span>
@@ -37,7 +37,7 @@ const PureInvitationAuthenticatorContent = ({
37
37
  return (
38
38
  <>
39
39
  <Input
40
- label={t('pin input label')}
40
+ label={t('auth code input label')}
41
41
  size='pin'
42
42
  length={pinLength}
43
43
  onChange={onChange}
@@ -57,6 +57,7 @@ const PureInvitationAuthenticatorContent = ({
57
57
  validationValence: 'error',
58
58
  validationMessage: t('failed to authenticate message')
59
59
  })}
60
+ data-testid='auth-code-input'
60
61
  />
61
62
  <div role='none' className='grow' />
62
63
  <div className='flex gap-2'>
@@ -65,6 +66,7 @@ const PureInvitationAuthenticatorContent = ({
65
66
  className='grow flex items-center gap-2 pli-2 order-2'
66
67
  onClick={onAuthenticate}
67
68
  data-autofocus-pinlength={invitationType}
69
+ data-testid={`${invitationType}-invitation-authenticator-next`}
68
70
  >
69
71
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
70
72
  <span className='grow'>{t('next label')}</span>
@@ -74,6 +76,7 @@ const PureInvitationAuthenticatorContent = ({
74
76
  disabled={disabled}
75
77
  className='flex items-center gap-2 pis-2 pie-4'
76
78
  onClick={() => dispatch({ type: 'cancel invitation', from: invitationType })}
79
+ data-testid={`${invitationType}-invitation-authenticator-cancel`}
77
80
  >
78
81
  <CaretLeft weight='bold' className={getSize(4)} />
79
82
  <span>{t('cancel label')}</span>
@@ -46,10 +46,16 @@ export const InvitationInput = ({ invitationType, ...viewStateProps }: Invitatio
46
46
  onKeyUp: ({ key }) => key === 'Enter' && handleNext()
47
47
  } as ComponentPropsWithoutRef<'input'>
48
48
  }}
49
+ data-testid='invitation-input'
49
50
  />
50
51
  <div role='none' className='grow' />
51
52
  <div className='flex gap-2'>
52
- <Button disabled={disabled} className='grow flex items-center gap-2 pli-2 order-2' onClick={handleNext}>
53
+ <Button
54
+ disabled={disabled}
55
+ className='grow flex items-center gap-2 pli-2 order-2'
56
+ onClick={handleNext}
57
+ data-testid={`${invitationType}-invitation-input-continue`}
58
+ >
53
59
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
54
60
  <span className='grow'>{t('continue label')}</span>
55
61
  <CaretRight weight='bold' className={getSize(4)} />
@@ -58,6 +64,7 @@ export const InvitationInput = ({ invitationType, ...viewStateProps }: Invitatio
58
64
  disabled={disabled}
59
65
  onClick={() => dispatch({ type: 'add identity' })}
60
66
  className='flex items-center gap-2 pis-2 pie-4'
67
+ data-testid={`${invitationType}-invitation-input-back`}
61
68
  >
62
69
  <CaretLeft weight='bold' className={getSize(4)} />
63
70
  <span>{t('back label')}</span>
@@ -35,7 +35,7 @@ const InvitationActions = ({
35
35
  <>
36
36
  <div role='none' className='grow' />
37
37
  <div className='flex gap-2'>
38
- <Button disabled className='grow flex items-center gap-2 pli-2 order-2'>
38
+ <Button disabled className='grow flex items-center gap-2 pli-2 order-2' data-testid='next'>
39
39
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
40
40
  <span className='grow'>{t('next label')}</span>
41
41
  <CaretRight weight='bold' className={getSize(4)} />
@@ -45,6 +45,7 @@ const InvitationActions = ({
45
45
  className='flex items-center gap-2 pis-2 pie-4'
46
46
  onClick={cancel}
47
47
  data-autofocus={`${invitationType} invitation acceptor; invitation rescuer`}
48
+ data-testid='invitation-rescuer-cancel'
48
49
  >
49
50
  <CaretLeft weight='bold' className={getSize(4)} />
50
51
  <span>{t('cancel label')}</span>
@@ -62,6 +63,7 @@ const InvitationActions = ({
62
63
  className='grow flex items-center gap-2 pli-2'
63
64
  onClick={() => dispatch({ type: 'reset invitation', from: invitationType })}
64
65
  data-autofocus={`${invitationType} invitation acceptor; invitation rescuer`}
66
+ data-testid='invitation-rescuer-reset'
65
67
  >
66
68
  <CaretLeft weight='bold' className={mx(getSize(5), 'invisible')} />
67
69
  <span className='grow'>{t('reset label')}</span>
@@ -82,6 +84,7 @@ export const InvitationRescuer = ({ invitationType, ...viewStateProps }: Invitat
82
84
  disabled={disabled}
83
85
  className='grow flex items-center gap-2 pli-2'
84
86
  data-autofocus='space invitation acceptor; invitation rescuer'
87
+ data-testid='invitation-rescuer-connect'
85
88
  >
86
89
  <CaretLeft weight='bold' className={mx(getSize(5), 'invisible')} />
87
90
  <span className='grow'>{t('connect label')}</span>