@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.
- package/dist/lib/browser/index.mjs +1781 -57
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/{IdentityListItem → IdentityList}/IdentityListItem.d.ts +0 -0
- package/dist/types/src/components/IdentityList/IdentityListItem.d.ts.map +1 -0
- package/dist/types/src/components/{IdentityListItem → IdentityList}/IdentityListItem.stories.d.ts +0 -0
- package/dist/types/src/components/IdentityList/IdentityListItem.stories.d.ts.map +1 -0
- package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts +6 -0
- package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts.map +1 -0
- package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts +7 -0
- package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts.map +1 -0
- package/dist/types/src/components/IdentityList/index.d.ts +4 -0
- package/dist/types/src/components/IdentityList/index.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationList.d.ts +7 -0
- package/dist/types/src/components/InvitationList/InvitationList.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationList.stories.d.ts +10 -0
- package/dist/types/src/components/InvitationList/InvitationList.stories.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts +7 -0
- package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationListItem.d.ts +9 -0
- package/dist/types/src/components/InvitationList/InvitationListItem.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationListProps.d.ts +4 -0
- package/dist/types/src/components/InvitationList/InvitationListProps.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts +9 -0
- package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts +9 -0
- package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts.map +1 -0
- package/dist/types/src/components/InvitationList/index.d.ts +5 -0
- package/dist/types/src/components/InvitationList/index.d.ts.map +1 -0
- package/dist/types/src/components/PanelSeparator/PanelSeparator.d.ts +3 -0
- package/dist/types/src/components/PanelSeparator/PanelSeparator.d.ts.map +1 -0
- package/dist/types/src/components/PanelSeparator/index.d.ts +2 -0
- package/dist/types/src/components/PanelSeparator/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +4 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts +6 -0
- package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts.map +1 -0
- package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts +10 -0
- package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -0
- package/dist/types/src/composites/JoinDialog/index.d.ts +2 -0
- package/dist/types/src/composites/JoinDialog/index.d.ts.map +1 -0
- package/dist/types/src/composites/index.d.ts +1 -0
- package/dist/types/src/composites/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts +14 -0
- package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts.map +1 -0
- package/dist/types/src/layouts/PanelAlertDialog/index.d.ts +2 -0
- package/dist/types/src/layouts/PanelAlertDialog/index.d.ts.map +1 -0
- package/dist/types/src/layouts/PanelSidebar/PanelSidebar.d.ts +2 -3
- package/dist/types/src/layouts/PanelSidebar/PanelSidebar.d.ts.map +1 -1
- package/dist/types/src/layouts/index.d.ts +1 -0
- package/dist/types/src/layouts/index.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +10 -0
- package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +3 -0
- package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +53 -0
- package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/index.d.ts +3 -0
- package/dist/types/src/panels/JoinPanel/index.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +7 -0
- package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts +7 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts +6 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +7 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts +10 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts +7 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts +6 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts +6 -0
- package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts +12 -0
- package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/index.d.ts +9 -0
- package/dist/types/src/panels/JoinPanel/view-states/index.d.ts.map +1 -0
- package/dist/types/src/panels/index.d.ts +1 -0
- package/dist/types/src/panels/index.d.ts.map +1 -1
- package/dist/types/src/styles/index.d.ts +1 -0
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/invitationStatusStyles.d.ts +20 -0
- package/dist/types/src/styles/invitationStatusStyles.d.ts.map +1 -0
- package/dist/types/src/styles/panelStyles.d.ts +2 -1
- package/dist/types/src/styles/panelStyles.d.ts.map +1 -1
- package/dist/types/src/translations/locales/en-US.d.ts +41 -0
- package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/invitationStatusValue.d.ts +8 -0
- package/dist/types/src/util/invitationStatusValue.d.ts.map +1 -0
- package/package.json +18 -11
- package/src/components/{IdentityListItem → IdentityList}/IdentityListItem.stories.tsx +0 -0
- package/src/components/{IdentityListItem → IdentityList}/IdentityListItem.tsx +4 -3
- package/src/components/IdentityList/SpaceMemberList.tsx +31 -0
- package/src/components/IdentityList/SpaceMemberListContainer.tsx +27 -0
- package/src/components/IdentityList/index.ts +7 -0
- package/src/components/InvitationList/InvitationList.stories.tsx +65 -0
- package/src/components/InvitationList/InvitationList.tsx +24 -0
- package/src/components/InvitationList/InvitationListContainer.tsx +27 -0
- package/src/components/InvitationList/InvitationListItem.tsx +95 -0
- package/src/components/InvitationList/InvitationListProps.ts +7 -0
- package/src/components/InvitationList/InvitationStatusAvatar.stories.tsx +52 -0
- package/src/components/InvitationList/InvitationStatusAvatar.tsx +75 -0
- package/src/components/InvitationList/index.ts +8 -0
- package/src/components/PanelSeparator/PanelSeparator.tsx +17 -0
- package/src/components/PanelSeparator/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/composites/JoinDialog/JoinDialog.stories.tsx +80 -0
- package/src/composites/JoinDialog/JoinDialog.tsx +31 -0
- package/src/composites/JoinDialog/index.ts +5 -0
- package/src/composites/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/layouts/PanelAlertDialog/PanelAlertDialog.tsx +44 -0
- package/src/layouts/PanelAlertDialog/index.ts +5 -0
- package/src/layouts/PanelPopover/PanelPopover.tsx +2 -2
- package/src/layouts/PanelSidebar/PanelSidebar.tsx +17 -25
- package/src/layouts/index.ts +1 -0
- package/src/panels/JoinPanel/JoinHeading.tsx +57 -0
- package/src/panels/JoinPanel/JoinPanel.tsx +303 -0
- package/src/panels/JoinPanel/JoinPanelProps.ts +94 -0
- package/src/panels/JoinPanel/index.ts +6 -0
- package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +69 -0
- package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +60 -0
- package/src/panels/JoinPanel/view-states/IdentityInput.tsx +72 -0
- package/src/panels/JoinPanel/view-states/IdentitySelector.tsx +98 -0
- package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +69 -0
- package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +139 -0
- package/src/panels/JoinPanel/view-states/InvitationInput.tsx +68 -0
- package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +95 -0
- package/src/panels/JoinPanel/view-states/ViewState.tsx +204 -0
- package/src/panels/JoinPanel/view-states/index.ts +12 -0
- package/src/panels/index.ts +1 -0
- package/src/styles/index.ts +1 -0
- package/src/styles/invitationStatusStyles.ts +42 -0
- package/src/styles/panelStyles.ts +2 -1
- package/src/testing/ClientProvider.tsx +1 -1
- package/src/translations/locales/en-US.ts +42 -1
- package/src/util/index.ts +5 -0
- package/src/util/invitationStatusValue.ts +21 -0
- package/theme-extensions.js +11 -0
- package/dist/types/src/components/IdentityListItem/IdentityListItem.d.ts.map +0 -1
- 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,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
|
+
};
|