@dxos/react-ui 0.1.23 → 0.1.24

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 (147) hide show
  1. package/dist/lib/browser/index.mjs +1781 -57
  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/{IdentityListItem → IdentityList}/IdentityListItem.d.ts +0 -0
  5. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts.map +1 -0
  6. package/dist/types/src/components/{IdentityListItem → IdentityList}/IdentityListItem.stories.d.ts +0 -0
  7. package/dist/types/src/components/IdentityList/IdentityListItem.stories.d.ts.map +1 -0
  8. package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts +6 -0
  9. package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts.map +1 -0
  10. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts +7 -0
  11. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts.map +1 -0
  12. package/dist/types/src/components/IdentityList/index.d.ts +4 -0
  13. package/dist/types/src/components/IdentityList/index.d.ts.map +1 -0
  14. package/dist/types/src/components/InvitationList/InvitationList.d.ts +7 -0
  15. package/dist/types/src/components/InvitationList/InvitationList.d.ts.map +1 -0
  16. package/dist/types/src/components/InvitationList/InvitationList.stories.d.ts +10 -0
  17. package/dist/types/src/components/InvitationList/InvitationList.stories.d.ts.map +1 -0
  18. package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts +7 -0
  19. package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts.map +1 -0
  20. package/dist/types/src/components/InvitationList/InvitationListItem.d.ts +9 -0
  21. package/dist/types/src/components/InvitationList/InvitationListItem.d.ts.map +1 -0
  22. package/dist/types/src/components/InvitationList/InvitationListProps.d.ts +4 -0
  23. package/dist/types/src/components/InvitationList/InvitationListProps.d.ts.map +1 -0
  24. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts +9 -0
  25. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts.map +1 -0
  26. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts +9 -0
  27. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts.map +1 -0
  28. package/dist/types/src/components/InvitationList/index.d.ts +5 -0
  29. package/dist/types/src/components/InvitationList/index.d.ts.map +1 -0
  30. package/dist/types/src/components/PanelSeparator/PanelSeparator.d.ts +3 -0
  31. package/dist/types/src/components/PanelSeparator/PanelSeparator.d.ts.map +1 -0
  32. package/dist/types/src/components/PanelSeparator/index.d.ts +2 -0
  33. package/dist/types/src/components/PanelSeparator/index.d.ts.map +1 -0
  34. package/dist/types/src/components/index.d.ts +4 -0
  35. package/dist/types/src/components/index.d.ts.map +1 -0
  36. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts +6 -0
  37. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts.map +1 -0
  38. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts +10 -0
  39. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -0
  40. package/dist/types/src/composites/JoinDialog/index.d.ts +2 -0
  41. package/dist/types/src/composites/JoinDialog/index.d.ts.map +1 -0
  42. package/dist/types/src/composites/index.d.ts +1 -0
  43. package/dist/types/src/composites/index.d.ts.map +1 -1
  44. package/dist/types/src/index.d.ts +1 -0
  45. package/dist/types/src/index.d.ts.map +1 -1
  46. package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts +14 -0
  47. package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts.map +1 -0
  48. package/dist/types/src/layouts/PanelAlertDialog/index.d.ts +2 -0
  49. package/dist/types/src/layouts/PanelAlertDialog/index.d.ts.map +1 -0
  50. package/dist/types/src/layouts/PanelSidebar/PanelSidebar.d.ts +2 -3
  51. package/dist/types/src/layouts/PanelSidebar/PanelSidebar.d.ts.map +1 -1
  52. package/dist/types/src/layouts/index.d.ts +1 -0
  53. package/dist/types/src/layouts/index.d.ts.map +1 -1
  54. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +10 -0
  55. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -0
  56. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +3 -0
  57. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -0
  58. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +53 -0
  59. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -0
  60. package/dist/types/src/panels/JoinPanel/index.d.ts +3 -0
  61. package/dist/types/src/panels/JoinPanel/index.d.ts.map +1 -0
  62. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +7 -0
  63. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -0
  64. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts +7 -0
  65. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts.map +1 -0
  66. package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts +6 -0
  67. package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts.map +1 -0
  68. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +7 -0
  69. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +1 -0
  70. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts +10 -0
  71. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -0
  72. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts +7 -0
  73. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -0
  74. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts +6 -0
  75. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -0
  76. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts +6 -0
  77. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -0
  78. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts +12 -0
  79. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts.map +1 -0
  80. package/dist/types/src/panels/JoinPanel/view-states/index.d.ts +9 -0
  81. package/dist/types/src/panels/JoinPanel/view-states/index.d.ts.map +1 -0
  82. package/dist/types/src/panels/index.d.ts +1 -0
  83. package/dist/types/src/panels/index.d.ts.map +1 -1
  84. package/dist/types/src/styles/index.d.ts +1 -0
  85. package/dist/types/src/styles/index.d.ts.map +1 -1
  86. package/dist/types/src/styles/invitationStatusStyles.d.ts +20 -0
  87. package/dist/types/src/styles/invitationStatusStyles.d.ts.map +1 -0
  88. package/dist/types/src/styles/panelStyles.d.ts +2 -1
  89. package/dist/types/src/styles/panelStyles.d.ts.map +1 -1
  90. package/dist/types/src/translations/locales/en-US.d.ts +41 -0
  91. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  92. package/dist/types/src/util/index.d.ts +2 -0
  93. package/dist/types/src/util/index.d.ts.map +1 -0
  94. package/dist/types/src/util/invitationStatusValue.d.ts +8 -0
  95. package/dist/types/src/util/invitationStatusValue.d.ts.map +1 -0
  96. package/package.json +18 -11
  97. package/src/components/{IdentityListItem → IdentityList}/IdentityListItem.stories.tsx +0 -0
  98. package/src/components/{IdentityListItem → IdentityList}/IdentityListItem.tsx +4 -3
  99. package/src/components/IdentityList/SpaceMemberList.tsx +31 -0
  100. package/src/components/IdentityList/SpaceMemberListContainer.tsx +27 -0
  101. package/src/components/IdentityList/index.ts +7 -0
  102. package/src/components/InvitationList/InvitationList.stories.tsx +65 -0
  103. package/src/components/InvitationList/InvitationList.tsx +24 -0
  104. package/src/components/InvitationList/InvitationListContainer.tsx +27 -0
  105. package/src/components/InvitationList/InvitationListItem.tsx +95 -0
  106. package/src/components/InvitationList/InvitationListProps.ts +7 -0
  107. package/src/components/InvitationList/InvitationStatusAvatar.stories.tsx +52 -0
  108. package/src/components/InvitationList/InvitationStatusAvatar.tsx +75 -0
  109. package/src/components/InvitationList/index.ts +8 -0
  110. package/src/components/PanelSeparator/PanelSeparator.tsx +17 -0
  111. package/src/components/PanelSeparator/index.ts +5 -0
  112. package/src/components/index.ts +7 -0
  113. package/src/composites/JoinDialog/JoinDialog.stories.tsx +80 -0
  114. package/src/composites/JoinDialog/JoinDialog.tsx +31 -0
  115. package/src/composites/JoinDialog/index.ts +5 -0
  116. package/src/composites/index.ts +1 -0
  117. package/src/index.ts +1 -0
  118. package/src/layouts/PanelAlertDialog/PanelAlertDialog.tsx +44 -0
  119. package/src/layouts/PanelAlertDialog/index.ts +5 -0
  120. package/src/layouts/PanelPopover/PanelPopover.tsx +2 -2
  121. package/src/layouts/PanelSidebar/PanelSidebar.tsx +17 -25
  122. package/src/layouts/index.ts +1 -0
  123. package/src/panels/JoinPanel/JoinHeading.tsx +57 -0
  124. package/src/panels/JoinPanel/JoinPanel.tsx +303 -0
  125. package/src/panels/JoinPanel/JoinPanelProps.ts +94 -0
  126. package/src/panels/JoinPanel/index.ts +6 -0
  127. package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +69 -0
  128. package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +60 -0
  129. package/src/panels/JoinPanel/view-states/IdentityInput.tsx +72 -0
  130. package/src/panels/JoinPanel/view-states/IdentitySelector.tsx +98 -0
  131. package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +69 -0
  132. package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +139 -0
  133. package/src/panels/JoinPanel/view-states/InvitationInput.tsx +68 -0
  134. package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +95 -0
  135. package/src/panels/JoinPanel/view-states/ViewState.tsx +204 -0
  136. package/src/panels/JoinPanel/view-states/index.ts +12 -0
  137. package/src/panels/index.ts +1 -0
  138. package/src/styles/index.ts +1 -0
  139. package/src/styles/invitationStatusStyles.ts +42 -0
  140. package/src/styles/panelStyles.ts +2 -1
  141. package/src/testing/ClientProvider.tsx +1 -1
  142. package/src/translations/locales/en-US.ts +42 -1
  143. package/src/util/index.ts +5 -0
  144. package/src/util/invitationStatusValue.ts +21 -0
  145. package/theme-extensions.js +11 -0
  146. package/dist/types/src/components/IdentityListItem/IdentityListItem.d.ts.map +0 -1
  147. package/dist/types/src/components/IdentityListItem/IdentityListItem.stories.d.ts.map +0 -1
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { cloneElement, Dispatch } from 'react';
6
+
7
+ import type {
8
+ AuthenticatingInvitationObservable,
9
+ Profile as NaturalProfile,
10
+ Space as NaturalSpace
11
+ } from '@dxos/client';
12
+ import { InvitationResult } from '@dxos/react-client';
13
+
14
+ export type Space = Pick<NaturalSpace, 'properties'>;
15
+
16
+ export type Profile = Pick<NaturalProfile, 'displayName' | 'identityKey'>;
17
+
18
+ export interface JoinPanelProps {
19
+ initialInvitationCode?: string;
20
+ titleId?: string;
21
+ exitActionParent?: Parameters<typeof cloneElement>[0];
22
+ onExit?: () => void;
23
+ doneActionParent?: Parameters<typeof cloneElement>[0];
24
+ onDone?: (result: InvitationResult | null) => void;
25
+ parseInvitationCodeInput?: (invitationCodeInput: string) => string;
26
+ }
27
+
28
+ export interface IdentityAction {
29
+ type: 'select identity' | 'added identity';
30
+ identity: Profile;
31
+ }
32
+
33
+ export interface EmptyJoinAction {
34
+ type: 'deselect identity' | 'cancel addition' | 'add identity';
35
+ }
36
+
37
+ export interface EmptyInvitationAction {
38
+ type:
39
+ | 'authenticating invitation'
40
+ | 'authenticate invitation'
41
+ | 'cancel invitation'
42
+ | 'cancelled invitation'
43
+ | 'connect invitation'
44
+ | 'fail invitation'
45
+ | 'accepted invitation'
46
+ | 'timeout invitation'
47
+ | 'reset invitation';
48
+ from: 'space' | 'halo';
49
+ }
50
+
51
+ export interface InvitationCodeAction {
52
+ type: 'connecting invitation';
53
+ from: 'space' | 'halo';
54
+ invitation: AuthenticatingInvitationObservable;
55
+ }
56
+
57
+ export type InvitationAction = EmptyInvitationAction | InvitationCodeAction;
58
+
59
+ export interface AdditionMethodAction {
60
+ type: 'select addition method';
61
+ method: 'recover identity' | 'accept device invitation' | 'create identity';
62
+ }
63
+
64
+ export type JoinAction = IdentityAction | EmptyJoinAction | AdditionMethodAction | InvitationAction;
65
+
66
+ export type JoinDispatch = Dispatch<JoinAction>;
67
+
68
+ export type InvitationView =
69
+ | 'invitation input'
70
+ | 'invitation rescuer'
71
+ | 'invitation authenticator'
72
+ | 'invitation accepted';
73
+
74
+ export type JoinView =
75
+ | 'identity selector'
76
+ | 'addition method selector'
77
+ | 'identity input'
78
+ | 'identity added'
79
+ | 'space invitation acceptor'
80
+ | 'halo invitation acceptor';
81
+
82
+ export interface JoinState {
83
+ activeView: JoinView;
84
+ unredeemedSpaceInvitationCode?: string;
85
+ spaceInvitation?: AuthenticatingInvitationObservable;
86
+ spaceViewState: InvitationView;
87
+ spaceInvitationAnnotation?: 'authenticating' | 'authentication failed';
88
+ unredeemedHaloInvitationCode?: string;
89
+ haloInvitation?: AuthenticatingInvitationObservable;
90
+ haloViewState: InvitationView;
91
+ haloInvitationAnnotation?: 'authenticating' | 'authentication failed';
92
+ selectedIdentity?: Profile;
93
+ additionMethod?: AdditionMethodAction['method'];
94
+ }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './JoinPanel';
6
+ export * from './JoinPanelProps';
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight, Plus, QrCode, Textbox } from 'phosphor-react';
6
+ import React from 'react';
7
+
8
+ import { Button, CompoundButton, getSize, mx, useTranslation } from '@dxos/react-components';
9
+
10
+ import { Profile } from '../JoinPanelProps';
11
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
+
13
+ export interface AdditionMethodSelectorProps extends ViewStateProps {
14
+ availableIdentities: Profile[];
15
+ }
16
+
17
+ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps }: AdditionMethodSelectorProps) => {
18
+ const disabled = !viewStateProps.active;
19
+ const { dispatch } = viewStateProps;
20
+
21
+ const { t } = useTranslation('os');
22
+
23
+ const sharedButtonProps = {
24
+ disabled,
25
+ after: <CaretRight className={getSize(4)} weight='bold' />,
26
+ slots: { label: { className: 'text-sm' } }
27
+ };
28
+
29
+ return (
30
+ <ViewState {...viewStateProps}>
31
+ <ViewStateHeading>{t('addition method selector title')}</ViewStateHeading>
32
+ <div role='none' className='flex flex-col gap-1 grow'>
33
+ <CompoundButton
34
+ {...sharedButtonProps}
35
+ description={t('create identity description')}
36
+ before={<Plus className={getSize(6)} />}
37
+ onClick={() => dispatch({ type: 'select addition method', method: 'create identity' })}
38
+ data-autofocus='addition method selector'
39
+ >
40
+ {t('create identity label')}
41
+ </CompoundButton>
42
+ <CompoundButton
43
+ {...sharedButtonProps}
44
+ description={t('join identity description')}
45
+ before={<QrCode className={getSize(6)} />}
46
+ onClick={() => dispatch({ type: 'select addition method', method: 'accept device invitation' })}
47
+ >
48
+ {t('join identity label')}
49
+ </CompoundButton>
50
+ <CompoundButton
51
+ {...sharedButtonProps}
52
+ description={t('recover identity description')}
53
+ before={<Textbox className={getSize(6)} />}
54
+ onClick={() => dispatch({ type: 'select addition method', method: 'recover identity' })}
55
+ >
56
+ {t('recover identity label')}
57
+ </CompoundButton>
58
+ </div>
59
+ <Button
60
+ disabled={disabled || availableIdentities.length < 1}
61
+ onClick={() => dispatch({ type: 'deselect identity' })}
62
+ >
63
+ <CaretLeft className={getSize(4)} weight='bold' />
64
+ <span className='grow'>{t('deselect identity label')}</span>
65
+ <CaretRight className={mx(getSize(4), 'invisible')} weight='bold' />
66
+ </Button>
67
+ </ViewState>
68
+ );
69
+ };
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight } from 'phosphor-react';
6
+ import React from 'react';
7
+
8
+ import type { Profile } from '@dxos/client';
9
+ import { Avatar, Button, getSize, mx, useTranslation } from '@dxos/react-components';
10
+
11
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
+
13
+ export interface IdentityAddedProps extends ViewStateProps {
14
+ addedIdentity?: Profile;
15
+ }
16
+
17
+ export const IdentityAdded = ({ addedIdentity, ...viewStateProps }: IdentityAddedProps) => {
18
+ const disabled = !viewStateProps.active;
19
+ const { dispatch } = viewStateProps;
20
+ const { t } = useTranslation('os');
21
+
22
+ return (
23
+ <ViewState {...viewStateProps}>
24
+ <ViewStateHeading>{t('identity added label')}</ViewStateHeading>
25
+ <div role='none' className='grow flex flex-col items-center justify-center text-center gap-2'>
26
+ <Avatar
27
+ size={20}
28
+ fallbackValue={addedIdentity?.identityKey.toHex() ?? ''}
29
+ label={
30
+ <p className={mx('text-lg', !addedIdentity?.displayName && 'font-mono')}>
31
+ {addedIdentity?.displayName ?? addedIdentity?.identityKey.truncate() ?? ' '}
32
+ </p>
33
+ }
34
+ variant='circle'
35
+ status='active'
36
+ />
37
+ </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>
58
+ </ViewState>
59
+ );
60
+ };
@@ -0,0 +1,72 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight } from 'phosphor-react';
6
+ import React, { ComponentPropsWithoutRef, useState } from 'react';
7
+
8
+ import { useClient } from '@dxos/react-client';
9
+ import { Button, getSize, Input, mx, useTranslation } from '@dxos/react-components';
10
+
11
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
+
13
+ export interface IdentityCreatorProps extends ViewStateProps {
14
+ method: 'recover identity' | 'create identity';
15
+ }
16
+
17
+ export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProps) => {
18
+ const disabled = !viewStateProps.active;
19
+ const { dispatch } = viewStateProps;
20
+ const { t } = useTranslation('os');
21
+ const [inputValue, setInputValue] = useState('');
22
+ const client = useClient();
23
+ const [validationMessage, setValidationMessage] = useState('');
24
+ const isRecover = method === 'recover identity';
25
+ const handleNext = () => {
26
+ void client.halo.createProfile({ [isRecover ? 'seedphrase' : 'displayName']: inputValue }).then(
27
+ (identity) => {
28
+ dispatch({ type: 'added identity', identity });
29
+ },
30
+ (_error) => {
31
+ setValidationMessage(t(isRecover ? 'failed to recover identity message' : 'failed to create identity message'));
32
+ }
33
+ );
34
+ };
35
+ return (
36
+ <ViewState {...viewStateProps}>
37
+ <Input
38
+ disabled={disabled}
39
+ label={
40
+ <ViewStateHeading>
41
+ {t(isRecover ? 'recover identity input label' : 'new identity input label')}
42
+ </ViewStateHeading>
43
+ }
44
+ onChange={({ target: { value } }) => setInputValue(value)}
45
+ slots={{
46
+ root: { className: 'm-0' },
47
+ input: {
48
+ 'data-autofocus': isRecover ? 'identity input; recover identity' : 'identity input; create identity',
49
+ onKeyUp: ({ key }) => key === 'Enter' && handleNext()
50
+ } as ComponentPropsWithoutRef<'input'>
51
+ }}
52
+ {...(validationMessage.length && { validationValence: 'error', validationMessage })}
53
+ />
54
+ <div role='none' className='grow' />
55
+ <div className='flex gap-2'>
56
+ <Button disabled={disabled} className='grow flex items-center gap-2 pli-2 order-2' onClick={handleNext}>
57
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
58
+ <span className='grow'>{t('continue label')}</span>
59
+ <CaretRight weight='bold' className={getSize(4)} />
60
+ </Button>
61
+ <Button
62
+ disabled={disabled}
63
+ onClick={() => dispatch({ type: 'add identity' })}
64
+ className='flex items-center gap-2 pis-2 pie-4'
65
+ >
66
+ <CaretLeft weight='bold' className={getSize(4)} />
67
+ <span>{t('back label')}</span>
68
+ </Button>
69
+ </div>
70
+ </ViewState>
71
+ );
72
+ };
@@ -0,0 +1,98 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import * as RadioGroup from '@radix-ui/react-radio-group';
6
+ import { CaretLeft, CaretRight, UserPlus } from 'phosphor-react';
7
+ import React, { useCallback, useState } from 'react';
8
+
9
+ import { Avatar, Button, themeVariantFocus, getSize, mx, useTranslation } from '@dxos/react-components';
10
+
11
+ import { Profile } from '../JoinPanelProps';
12
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
13
+
14
+ export interface IdentitySelectorProps extends ViewStateProps {
15
+ availableIdentities: Profile[];
16
+ }
17
+
18
+ export const IdentitySelector = ({ availableIdentities, ...viewStateProps }: IdentitySelectorProps) => {
19
+ const { dispatch } = viewStateProps;
20
+ const disabled = !viewStateProps.active;
21
+
22
+ const { t } = useTranslation('os');
23
+ const [activeIdentity, setActiveIdentity] = useState(availableIdentities[0]);
24
+ const onValueChange = useCallback(
25
+ (nextHex: string) =>
26
+ setActiveIdentity(
27
+ availableIdentities.find((identity) => identity.identityKey.toHex() === nextHex) || activeIdentity
28
+ ),
29
+ [availableIdentities]
30
+ );
31
+
32
+ return (
33
+ <ViewState {...viewStateProps}>
34
+ <ViewStateHeading>{t('identity selector title')}</ViewStateHeading>
35
+ <RadioGroup.Root
36
+ disabled={disabled}
37
+ value={activeIdentity?.identityKey.toHex()}
38
+ onValueChange={onValueChange}
39
+ className='overflow-y-auto grow shrink min-bs-10 pli-1 -mli-1'
40
+ aria-label={t('identity radio group title')}
41
+ data-autofocus='identity selector'
42
+ >
43
+ {availableIdentities.map(({ displayName, identityKey }) => {
44
+ const hex = identityKey.toHex();
45
+ const inputId = `identitySelector__item--${hex}`;
46
+ const labelId = `identitySelector__itemLabel--${hex}`;
47
+ return (
48
+ <label htmlFor={inputId} key={hex} className='flex items-center mbe-2 gap-2 cursor-pointer'>
49
+ <RadioGroup.Item
50
+ id={inputId}
51
+ aria-labelledby={labelId}
52
+ value={hex}
53
+ className={mx(
54
+ 'peer relative w-4 h-4 rounded-full',
55
+ 'border border-transparent text-white',
56
+ 'radix-state-checked:bg-primary-600',
57
+ 'radix-state-unchecked:bg-neutral-100 dark:radix-state-unchecked:bg-neutral-900',
58
+ themeVariantFocus('os')
59
+ )}
60
+ >
61
+ <RadioGroup.Indicator className='absolute inset-0 flex items-center justify-center leading-0'>
62
+ <div className='w-1.5 h-1.5 rounded-full bg-white' />
63
+ </RadioGroup.Indicator>
64
+ </RadioGroup.Item>
65
+ <Avatar fallbackValue={hex} labelId={labelId} variant='circle' />
66
+ <span id={labelId} className={mx(!displayName && 'font-mono')}>
67
+ {displayName ?? identityKey.truncate() ?? ''}
68
+ </span>
69
+ </label>
70
+ );
71
+ })}
72
+ </RadioGroup.Root>
73
+ <Button
74
+ disabled={disabled}
75
+ compact
76
+ onClick={() => dispatch({ type: 'add identity' })}
77
+ className='flex items-center gap-2 pli-2'
78
+ >
79
+ <span>{t('add identity label')}</span>
80
+ <UserPlus weight='bold' className={getSize(3.5)} />
81
+ </Button>
82
+ <Button
83
+ disabled={disabled || !activeIdentity}
84
+ className='flex items-center gap-2 pli-2'
85
+ onClick={() =>
86
+ dispatch({
87
+ type: 'select identity',
88
+ identity: activeIdentity
89
+ })
90
+ }
91
+ >
92
+ <CaretLeft weight='bold' className={mx(getSize(4), 'invisible')} />
93
+ <span className='grow'>{t('continue label')}</span>
94
+ <CaretRight weight='bold' className={getSize(4)} />
95
+ </Button>
96
+ </ViewState>
97
+ );
98
+ };
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, Check } from 'phosphor-react';
6
+ import React, { cloneElement } from 'react';
7
+
8
+ import { AuthenticatingInvitationObservable } from '@dxos/client';
9
+ import { InvitationResult, useInvitationStatus } from '@dxos/react-client';
10
+ import { Button, getSize, mx, useTranslation } from '@dxos/react-components';
11
+
12
+ import { ViewState, ViewStateProps } from './ViewState';
13
+
14
+ export interface InvitationAcceptedProps extends ViewStateProps {
15
+ invitationType: 'space' | 'halo';
16
+ doneActionParent?: Parameters<typeof cloneElement>[0];
17
+ onDone?: (result: InvitationResult | null) => void;
18
+ }
19
+
20
+ const PureInvitationAcceptedContent = ({
21
+ onDone,
22
+ result,
23
+ invitationType,
24
+ doneActionParent,
25
+ active
26
+ }: InvitationAcceptedProps & { result: InvitationResult | null }) => {
27
+ const disabled = !active;
28
+ const { t } = useTranslation('os');
29
+
30
+ const doneButton = (
31
+ <Button
32
+ {...(onDone && { onClick: () => onDone(result) })}
33
+ disabled={disabled}
34
+ className='grow flex items-center gap-2 pli-2'
35
+ data-autofocus={`${invitationType} invitation acceptor; invitation accepted`}
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
+ const InvitationAcceptedContent = (props: InvitationAcceptedProps) => {
47
+ const { result } = useInvitationStatus(props.activeInvitation as AuthenticatingInvitationObservable);
48
+ return <PureInvitationAcceptedContent {...props} result={result} />;
49
+ };
50
+
51
+ export const InvitationAccepted = (props: InvitationAcceptedProps) => {
52
+ const {
53
+ invitationType: _invitationType,
54
+ doneActionParent: _doneActionParent,
55
+ onDone: _onDone,
56
+ ...viewStateProps
57
+ } = props;
58
+ const { activeInvitation } = viewStateProps;
59
+
60
+ return (
61
+ <ViewState {...viewStateProps}>
62
+ {!activeInvitation || activeInvitation === true ? (
63
+ <PureInvitationAcceptedContent {...props} result={null} />
64
+ ) : (
65
+ <InvitationAcceptedContent {...props} />
66
+ )}
67
+ </ViewState>
68
+ );
69
+ };
@@ -0,0 +1,139 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight } from 'phosphor-react';
6
+ import React, { ChangeEvent, ComponentProps, ComponentPropsWithoutRef, useCallback, useState } from 'react';
7
+
8
+ import { AuthenticatingInvitationObservable } from '@dxos/client';
9
+ import { useInvitationStatus } from '@dxos/react-client';
10
+ import { Button, getSize, Input, mx, useTranslation } from '@dxos/react-components';
11
+
12
+ import { ViewState, ViewStateProps } from './ViewState';
13
+
14
+ const pinLength = 6;
15
+
16
+ export interface InvitationAuthenticatorProps extends ViewStateProps {
17
+ invitationType: 'space' | 'halo';
18
+ failed?: boolean;
19
+ }
20
+
21
+ const PureInvitationAuthenticatorContent = ({
22
+ disabled,
23
+ failed,
24
+ dispatch,
25
+ invitationType,
26
+ onChange,
27
+ onAuthenticate
28
+ }: {
29
+ disabled?: boolean;
30
+ dispatch: ViewStateProps['dispatch'];
31
+ invitationType: InvitationAuthenticatorProps['invitationType'];
32
+ failed: InvitationAuthenticatorProps['failed'];
33
+ onChange: ComponentProps<typeof Input>['onChange'];
34
+ onAuthenticate: ComponentProps<typeof Button>['onClick'];
35
+ }) => {
36
+ const { t } = useTranslation('os');
37
+ return (
38
+ <>
39
+ <Input
40
+ label={t('pin input label')}
41
+ size='pin'
42
+ length={pinLength}
43
+ onChange={onChange}
44
+ disabled={disabled}
45
+ slots={{
46
+ root: { className: 'm-0' },
47
+ label: { className: 'sr-only' },
48
+ description: { className: 'text-center' },
49
+ input: {
50
+ disabled,
51
+ inputMode: 'numeric',
52
+ pattern: '\\d*',
53
+ 'data-autofocus': `${invitationType} invitation acceptor; invitation authenticator`
54
+ } as ComponentPropsWithoutRef<'input'>
55
+ }}
56
+ {...(failed && {
57
+ validationValence: 'error',
58
+ validationMessage: t('failed to authenticate message')
59
+ })}
60
+ />
61
+ <div role='none' className='grow' />
62
+ <div className='flex gap-2'>
63
+ <Button
64
+ disabled={disabled}
65
+ className='grow flex items-center gap-2 pli-2 order-2'
66
+ onClick={onAuthenticate}
67
+ data-autofocus-pinlength={invitationType}
68
+ >
69
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
70
+ <span className='grow'>{t('next label')}</span>
71
+ <CaretRight weight='bold' className={getSize(4)} />
72
+ </Button>
73
+ <Button
74
+ disabled={disabled}
75
+ className='flex items-center gap-2 pis-2 pie-4'
76
+ onClick={() => dispatch({ type: 'cancel invitation', from: invitationType })}
77
+ >
78
+ <CaretLeft weight='bold' className={getSize(4)} />
79
+ <span>{t('cancel label')}</span>
80
+ </Button>
81
+ </div>
82
+ </>
83
+ );
84
+ };
85
+
86
+ const InvitationAuthenticatorContent = ({
87
+ disabled,
88
+ invitation,
89
+ dispatch,
90
+ invitationType,
91
+ failed
92
+ }: {
93
+ disabled?: boolean;
94
+ invitation: AuthenticatingInvitationObservable;
95
+ dispatch: ViewStateProps['dispatch'];
96
+ invitationType: InvitationAuthenticatorProps['invitationType'];
97
+ failed: InvitationAuthenticatorProps['failed'];
98
+ }) => {
99
+ const [pinValue, setPinValue] = useState('');
100
+ const { authenticate } = useInvitationStatus(invitation);
101
+ const onAuthenticate = useCallback(() => {
102
+ dispatch({ type: 'authenticating invitation', from: invitationType });
103
+ void authenticate(pinValue);
104
+ }, [dispatch, invitationType, authenticate, pinValue]);
105
+ const onChange = useCallback(
106
+ ({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
107
+ setPinValue(value);
108
+ if (value.length === pinLength) {
109
+ (document.querySelector(`[data-autofocus-pinlength="${invitationType}"]`) as HTMLElement | null)?.focus();
110
+ }
111
+ },
112
+ [authenticate, pinValue]
113
+ );
114
+ return (
115
+ <PureInvitationAuthenticatorContent {...{ disabled, failed, dispatch, invitationType, onChange, onAuthenticate }} />
116
+ );
117
+ };
118
+
119
+ export const InvitationAuthenticator = ({
120
+ invitationType,
121
+ failed,
122
+ ...viewStateProps
123
+ }: InvitationAuthenticatorProps) => {
124
+ const disabled = !viewStateProps.active;
125
+ const { activeInvitation, dispatch } = viewStateProps;
126
+ return (
127
+ <ViewState {...viewStateProps}>
128
+ {!activeInvitation || activeInvitation === true ? (
129
+ <PureInvitationAuthenticatorContent
130
+ {...{ disabled, failed, dispatch, invitationType, onChange: () => {}, onAuthenticate: () => {} }}
131
+ />
132
+ ) : (
133
+ <InvitationAuthenticatorContent
134
+ {...{ disabled, failed, invitation: activeInvitation, dispatch, invitationType }}
135
+ />
136
+ )}
137
+ </ViewState>
138
+ );
139
+ };
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight } from 'phosphor-react';
6
+ import React, { ComponentPropsWithoutRef, useState } from 'react';
7
+
8
+ import { InvitationEncoder } from '@dxos/client';
9
+ import { useClient } from '@dxos/react-client';
10
+ import { Button, getSize, Input, mx, useTranslation } from '@dxos/react-components';
11
+
12
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
13
+
14
+ export interface InvitationInputProps extends ViewStateProps {
15
+ invitationType: 'space' | 'halo';
16
+ }
17
+
18
+ export const InvitationInput = ({ invitationType, ...viewStateProps }: InvitationInputProps) => {
19
+ const client = useClient();
20
+ const disabled = !viewStateProps.active;
21
+ const { dispatch } = viewStateProps;
22
+ const { t } = useTranslation('os');
23
+
24
+ const [inputValue, setInputValue] = useState('');
25
+
26
+ const handleNext = () =>
27
+ dispatch({
28
+ type: 'connecting invitation',
29
+ from: invitationType,
30
+ invitation: client[invitationType === 'halo' ? 'halo' : 'echo'].acceptInvitation(
31
+ InvitationEncoder.decode(inputValue)
32
+ )
33
+ });
34
+
35
+ return (
36
+ <ViewState {...viewStateProps}>
37
+ <Input
38
+ disabled={disabled}
39
+ label={<ViewStateHeading>{t('invitation input label')}</ViewStateHeading>}
40
+ onChange={({ target: { value } }) => setInputValue(value)}
41
+ slots={{
42
+ root: { className: 'm-0' },
43
+ label: { className: 'sr-only' },
44
+ input: {
45
+ 'data-autofocus': `${invitationType} invitation acceptor; invitation input`,
46
+ onKeyUp: ({ key }) => key === 'Enter' && handleNext()
47
+ } as ComponentPropsWithoutRef<'input'>
48
+ }}
49
+ />
50
+ <div role='none' className='grow' />
51
+ <div className='flex gap-2'>
52
+ <Button disabled={disabled} className='grow flex items-center gap-2 pli-2 order-2' onClick={handleNext}>
53
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
54
+ <span className='grow'>{t('continue label')}</span>
55
+ <CaretRight weight='bold' className={getSize(4)} />
56
+ </Button>
57
+ <Button
58
+ disabled={disabled}
59
+ onClick={() => dispatch({ type: 'add identity' })}
60
+ className='flex items-center gap-2 pis-2 pie-4'
61
+ >
62
+ <CaretLeft weight='bold' className={getSize(4)} />
63
+ <span>{t('back label')}</span>
64
+ </Button>
65
+ </div>
66
+ </ViewState>
67
+ );
68
+ };