@dxos/react-ui 0.1.26 → 0.1.27-next.a0dff21

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 (142) hide show
  1. package/dist/lib/browser/index.mjs +1021 -472
  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 +4 -3
  7. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts.map +1 -1
  8. package/dist/types/src/components/IdentityList/IdentityListItem.stories.d.ts +3 -2
  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/IdentityPopover/IdentityPopover.d.ts +2 -2
  32. package/dist/types/src/composites/IdentityPopover/IdentityPopover.d.ts.map +1 -1
  33. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts +1 -1
  34. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts.map +1 -1
  35. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts +2 -2
  36. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -1
  37. package/dist/types/src/composites/Shell/Shell.d.ts +6 -0
  38. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -0
  39. package/dist/types/src/composites/Shell/index.d.ts +2 -0
  40. package/dist/types/src/composites/Shell/index.d.ts.map +1 -0
  41. package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts +6 -0
  42. package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts.map +1 -0
  43. package/dist/types/src/composites/SpaceDialog/index.d.ts +2 -0
  44. package/dist/types/src/composites/SpaceDialog/index.d.ts.map +1 -0
  45. package/dist/types/src/composites/index.d.ts +2 -0
  46. package/dist/types/src/composites/index.d.ts.map +1 -1
  47. package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts.map +1 -1
  48. package/dist/types/src/layouts/PanelDialog/PanelDialog.d.ts +14 -0
  49. package/dist/types/src/layouts/PanelDialog/PanelDialog.d.ts.map +1 -0
  50. package/dist/types/src/layouts/PanelDialog/index.d.ts +2 -0
  51. package/dist/types/src/layouts/PanelDialog/index.d.ts.map +1 -0
  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/DevicesPanel/DevicesPanel.d.ts +10 -0
  55. package/dist/types/src/panels/DevicesPanel/DevicesPanel.d.ts.map +1 -0
  56. package/dist/types/src/panels/DevicesPanel/index.d.ts +2 -0
  57. package/dist/types/src/panels/DevicesPanel/index.d.ts.map +1 -0
  58. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts +2 -2
  59. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  60. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +3 -0
  61. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -1
  62. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +1 -1
  63. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -1
  64. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +6 -5
  65. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -1
  66. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +2 -2
  67. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -1
  68. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts +12 -4
  69. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts.map +1 -1
  70. package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts.map +1 -1
  71. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +2 -2
  72. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +1 -1
  73. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -1
  74. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -1
  75. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -1
  76. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -1
  77. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts +2 -2
  78. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts.map +1 -1
  79. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts +12 -0
  80. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -0
  81. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts +9 -0
  82. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -0
  83. package/dist/types/src/panels/SpacePanel/index.d.ts +2 -0
  84. package/dist/types/src/panels/SpacePanel/index.d.ts.map +1 -0
  85. package/dist/types/src/panels/index.d.ts +2 -0
  86. package/dist/types/src/panels/index.d.ts.map +1 -1
  87. package/dist/types/src/styles/dialogStyles.d.ts +3 -0
  88. package/dist/types/src/styles/dialogStyles.d.ts.map +1 -0
  89. package/dist/types/src/styles/index.d.ts +1 -0
  90. package/dist/types/src/styles/index.d.ts.map +1 -1
  91. package/dist/types/src/translations/locales/en-US.d.ts +12 -1
  92. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  93. package/package.json +12 -11
  94. package/src/components/IdentityList/DeviceList.tsx +31 -0
  95. package/src/components/IdentityList/IdentityListItem.tsx +17 -5
  96. package/src/components/IdentityList/SpaceMemberList.tsx +10 -8
  97. package/src/components/IdentityList/SpaceMemberListContainer.tsx +7 -6
  98. package/src/components/IdentityList/index.ts +1 -0
  99. package/src/components/InvitationList/InvitationList.stories.tsx +1 -1
  100. package/src/components/InvitationList/InvitationList.tsx +12 -6
  101. package/src/components/InvitationList/InvitationListItem.tsx +30 -25
  102. package/src/components/InvitationList/InvitationStatusAvatar.tsx +10 -5
  103. package/src/components/SpaceList/SpaceListItem.tsx +32 -0
  104. package/src/components/SpaceList/index.ts +5 -0
  105. package/src/components/index.ts +1 -0
  106. package/src/composites/DevicesDialog/DevicesDialog.tsx +38 -0
  107. package/src/composites/DevicesDialog/index.ts +5 -0
  108. package/src/composites/IdentityPopover/IdentityPopover.tsx +3 -3
  109. package/src/composites/JoinDialog/JoinDialog.stories.tsx +59 -58
  110. package/src/composites/JoinDialog/JoinDialog.tsx +3 -1
  111. package/src/composites/Shell/Shell.tsx +86 -0
  112. package/src/composites/Shell/index.ts +5 -0
  113. package/src/composites/SpaceDialog/SpaceDialog.tsx +38 -0
  114. package/src/composites/SpaceDialog/index.ts +5 -0
  115. package/src/composites/index.ts +3 -0
  116. package/src/layouts/PanelAlertDialog/PanelAlertDialog.tsx +4 -8
  117. package/src/layouts/PanelDialog/PanelDialog.tsx +39 -0
  118. package/src/layouts/PanelDialog/index.ts +5 -0
  119. package/src/layouts/PanelSidebar/PanelSidebar.tsx +1 -1
  120. package/src/layouts/index.ts +1 -0
  121. package/src/panels/DevicesPanel/DevicesPanel.tsx +101 -0
  122. package/src/panels/DevicesPanel/index.ts +5 -0
  123. package/src/panels/IdentityPanel/IdentityPanel.tsx +5 -5
  124. package/src/panels/JoinPanel/JoinHeading.tsx +38 -16
  125. package/src/panels/JoinPanel/JoinPanel.tsx +35 -14
  126. package/src/panels/JoinPanel/JoinPanelProps.ts +6 -10
  127. package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +8 -2
  128. package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +67 -28
  129. package/src/panels/JoinPanel/view-states/IdentityInput.tsx +9 -2
  130. package/src/panels/JoinPanel/view-states/IdentitySelector.tsx +8 -6
  131. package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +1 -0
  132. package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +4 -1
  133. package/src/panels/JoinPanel/view-states/InvitationInput.tsx +8 -1
  134. package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +4 -1
  135. package/src/panels/JoinPanel/view-states/ViewState.tsx +3 -3
  136. package/src/panels/SpacePanel/SpacePanel.stories.tsx +53 -0
  137. package/src/panels/SpacePanel/SpacePanel.tsx +90 -0
  138. package/src/panels/SpacePanel/index.ts +5 -0
  139. package/src/panels/index.ts +2 -0
  140. package/src/styles/dialogStyles.ts +8 -0
  141. package/src/styles/index.ts +1 -0
  142. package/src/translations/locales/en-US.ts +14 -3
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import * as Dialog from '@radix-ui/react-dialog';
6
+ import React, { ComponentPropsWithoutRef, PropsWithChildren } from 'react';
7
+
8
+ import { mx } from '@dxos/react-components';
9
+
10
+ import { defaultDialogContent, defaultOverlay } from '../../styles';
11
+
12
+ export interface PanelDialogSlots {
13
+ root: ComponentPropsWithoutRef<typeof Dialog.Root>;
14
+ overlay: ComponentPropsWithoutRef<typeof Dialog.Overlay>;
15
+ content: ComponentPropsWithoutRef<typeof Dialog.Content>;
16
+ trigger: ComponentPropsWithoutRef<typeof Dialog.Trigger>;
17
+ }
18
+
19
+ export type PanelDialogProps = PropsWithChildren<{
20
+ titleId: string;
21
+ slots?: Partial<PanelDialogSlots>;
22
+ }>;
23
+
24
+ export const PanelDialog = ({ titleId, slots = {}, children }: PanelDialogProps) => {
25
+ return (
26
+ <Dialog.Root defaultOpen {...slots.root}>
27
+ {slots.trigger && <Dialog.Trigger {...slots.trigger} />}
28
+ <Dialog.Overlay {...slots.overlay} className={mx(defaultOverlay, 'z-40', slots.overlay?.className)}>
29
+ <Dialog.Content
30
+ {...slots.content}
31
+ aria-labelledby={titleId}
32
+ className={mx(defaultDialogContent, slots.content?.className)}
33
+ >
34
+ {children}
35
+ </Dialog.Content>
36
+ </Dialog.Overlay>
37
+ </Dialog.Root>
38
+ );
39
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './PanelDialog';
@@ -82,7 +82,7 @@ export const PanelSidebarProvider = ({ children, slots }: PropsWithChildren<Pane
82
82
  onCloseAutoFocus={(event) => isLg && event.preventDefault()}
83
83
  {...slots?.content}
84
84
  className={mx(
85
- 'fixed block-start-0 block-end-0 is-sidebar z-50 overscroll-contain overflow-x-hidden overflow-y-auto',
85
+ 'fixed block-start-0 block-end-0 is-sidebar z-30 overscroll-contain overflow-x-hidden overflow-y-auto',
86
86
  'transition-[inset-inline-start,inset-inline-end] duration-200 ease-in-out',
87
87
  transitionShow ? 'inline-start-0' : '-inline-start-sidebar',
88
88
  slots?.content?.className
@@ -5,3 +5,4 @@
5
5
  export * from './PanelPopover';
6
6
  export * from './PanelSidebar';
7
7
  export * from './PanelAlertDialog';
8
+ export * from './PanelDialog';
@@ -0,0 +1,101 @@
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, DensityProvider, 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?.profile?.displayName;
30
+
31
+ if (!identity) {
32
+ return null;
33
+ }
34
+
35
+ const doneButton = (
36
+ <Button 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
+ onClick={() => client.halo.createInvitation()}
61
+ data-testid='create-space-invitation'
62
+ >
63
+ <span>{t('create space invitation label')}</span>
64
+ <UserPlus className={getSize(4)} weight='bold' />
65
+ </Button>
66
+ <PanelSeparator />
67
+ <DeviceList devices={devices} />
68
+ </div>
69
+ </div>
70
+ );
71
+ };
72
+
73
+ type DevicesPanelState = {
74
+ activeView: DevicesView;
75
+ };
76
+
77
+ type DevicesPanelAction = {
78
+ type: null;
79
+ };
80
+
81
+ // TODO(wittjosiah): Rename identity panel and include other views?
82
+ export const DevicesPanel = (props: DevicesPanelProps) => {
83
+ const reducer = (state: DevicesPanelState, action: DevicesPanelAction) => {
84
+ const nextState = { ...state };
85
+ switch (action.type) {
86
+ case null:
87
+ }
88
+ return nextState;
89
+ };
90
+
91
+ const [panelState] = useReducer(reducer, {
92
+ activeView: 'device list'
93
+ });
94
+
95
+ // TODO(wittjosiah): Use ViewState or similar.
96
+ return (
97
+ <DensityProvider density='fine'>
98
+ {panelState.activeView === 'device list' ? <DeviceListView {...props} /> : null}
99
+ </DensityProvider>
100
+ );
101
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './DevicesPanel';
@@ -3,7 +3,7 @@
3
3
  //
4
4
  import React from 'react';
5
5
 
6
- import type { Profile } from '@dxos/client';
6
+ import type { Identity } from '@dxos/client';
7
7
  import { useClient } from '@dxos/react-client';
8
8
  import { Avatar, Button, ThemeContext, useTranslation } from '@dxos/react-components';
9
9
 
@@ -11,7 +11,7 @@ export const IdentityPanel = ({
11
11
  identity,
12
12
  onClickManageProfile
13
13
  }: {
14
- identity: Profile;
14
+ identity: Identity;
15
15
  onClickManageProfile?: () => void;
16
16
  }) => {
17
17
  const { t } = useTranslation('os');
@@ -22,15 +22,15 @@ export const IdentityPanel = ({
22
22
  tab?.focus();
23
23
  };
24
24
  return (
25
- <ThemeContext.Provider value={{ themeVariant: 'os' }}>
25
+ <ThemeContext.Provider value={{ themeVariant: 'os', rootDensity: 'fine' }}>
26
26
  <div className='flex flex-col gap-2 justify-center items-center'>
27
27
  <Avatar
28
28
  size={16}
29
29
  variant='circle'
30
30
  fallbackValue={identity.identityKey.toHex()}
31
- label={identity.displayName ?? ''}
31
+ label={identity.profile?.displayName ?? ''}
32
32
  />
33
- <Button compact onClick={onClickManageProfile ?? defaultManageProfile} className='is-full'>
33
+ <Button onClick={onClickManageProfile ?? defaultManageProfile} className='is-full'>
34
34
  {t('manage profile label')}
35
35
  </Button>
36
36
  </div>
@@ -9,25 +9,35 @@ 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 variant='ghost' {...(onExit && { onClick: onExit })} className='grow-0 shrink-0' data-testid='join-exit'>
31
41
  <ProhibitInset className={getSize(5)} />
32
42
  <span className='sr-only'>{t('exit label')}</span>
33
43
  </Button>
@@ -36,20 +46,32 @@ export const JoinHeading = forwardRef(
36
46
  return (
37
47
  <div role='none' className={mx(subduedSurface, 'p-2 rounded-bs-md')} ref={ref}>
38
48
  <div role='group' className='flex items-center gap-2'>
39
- <Avatar fallbackValue={invitation?.invitation?.spaceKey?.toHex() ?? ''} labelId={nameId} />
49
+ {invitationKey ? (
50
+ <Avatar fallbackValue={invitationKey} labelId={nameId} />
51
+ ) : (
52
+ <span role='none' className={mx(getSize(10), 'bg-neutral-300 dark:bg-neutral-700 rounded-full')} />
53
+ )}
40
54
  <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
- />
55
+ {invitation ? (
56
+ <Trans
57
+ {...{
58
+ defaults: t('joining heading'),
59
+ components: {
60
+ small: <span className='block leading-none mbe-1 font-system-medium text-sm' />,
61
+ large: <span className='block leading-none' id={nameId} />
62
+ },
63
+ values: { name }
64
+ }}
65
+ />
66
+ ) : (
67
+ <span className='block leading-none font-system-medium text-sm'>
68
+ {mode === 'halo-only' ? t('halo heading') : t('join space heading')}
69
+ </span>
70
+ )}
51
71
  </Heading>
52
- {exitActionParent ? cloneElement(exitActionParent, {}, exitButton) : exitButton}
72
+ {!preventExit &&
73
+ mode !== 'halo-only' &&
74
+ (exitActionParent ? cloneElement(exitActionParent, {}, exitButton) : exitButton)}
53
75
  </div>
54
76
  </div>
55
77
  );
@@ -1,12 +1,12 @@
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';
8
8
  import { useClient, useIdentity } from '@dxos/react-client';
9
- import { useId } from '@dxos/react-components';
9
+ import { DensityProvider, useId } from '@dxos/react-components';
10
10
 
11
11
  import { JoinHeading } from './JoinHeading';
12
12
  import { JoinAction, JoinPanelProps, JoinState } from './JoinPanelProps';
@@ -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();
@@ -176,8 +190,10 @@ export const JoinPanel = ({
176
190
  }, [joinState.haloInvitation]);
177
191
 
178
192
  return (
179
- <>
180
- <JoinHeading {...{ titleId, invitation: joinState.spaceInvitation, onExit, exitActionParent }} />
193
+ <DensityProvider density='fine'>
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
@@ -298,6 +319,6 @@ export const JoinPanel = ({
298
319
  />
299
320
  </div>
300
321
  </div>
301
- </>
322
+ </DensityProvider>
302
323
  );
303
324
  };
@@ -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, Identity } 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;
@@ -27,7 +23,7 @@ export interface JoinPanelProps {
27
23
 
28
24
  export interface IdentityAction {
29
25
  type: 'select identity' | 'added identity';
30
- identity: Profile;
26
+ identity: Identity;
31
27
  }
32
28
 
33
29
  export interface EmptyJoinAction {
@@ -89,6 +85,6 @@ export interface JoinState {
89
85
  haloInvitation?: AuthenticatingInvitationObservable;
90
86
  haloViewState: InvitationView;
91
87
  haloInvitationAnnotation?: 'authenticating' | 'authentication failed';
92
- selectedIdentity?: Profile;
88
+ selectedIdentity?: Identity;
93
89
  additionMethod?: AdditionMethodAction['method'];
94
90
  }
@@ -5,13 +5,13 @@
5
5
  import { CaretLeft, CaretRight, Plus, QrCode, Textbox } from 'phosphor-react';
6
6
  import React from 'react';
7
7
 
8
+ import type { Identity } 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 {
14
- availableIdentities: Profile[];
14
+ availableIdentities: Identity[];
15
15
  }
16
16
 
17
17
  export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps }: AdditionMethodSelectorProps) => {
@@ -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,14 +45,18 @@ 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>
50
52
  <CompoundButton
51
53
  {...sharedButtonProps}
54
+ // TODO(mykola): Implement recover.
55
+ disabled={true}
52
56
  description={t('recover identity description')}
53
57
  before={<Textbox className={getSize(6)} />}
54
58
  onClick={() => dispatch({ type: 'select addition method', method: 'recover identity' })}
59
+ data-testid='recover-identity'
55
60
  >
56
61
  {t('recover identity label')}
57
62
  </CompoundButton>
@@ -59,6 +64,7 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
59
64
  <Button
60
65
  disabled={disabled || availableIdentities.length < 1}
61
66
  onClick={() => dispatch({ type: 'deselect identity' })}
67
+ data-testid='deselect-identity'
62
68
  >
63
69
  <CaretLeft className={getSize(4)} weight='bold' />
64
70
  <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
- import type { Profile } from '@dxos/client';
8
+ import type { Identity } 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 {
14
- addedIdentity?: Profile;
15
+ export interface IdentityAddedProps extends ViewStateProps, DoneProps {
16
+ mode?: JoinPanelMode;
17
+ addedIdentity?: Identity;
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');
@@ -27,34 +62,38 @@ export const IdentityAdded = ({ addedIdentity, ...viewStateProps }: IdentityAdde
27
62
  size={20}
28
63
  fallbackValue={addedIdentity?.identityKey.toHex() ?? ''}
29
64
  label={
30
- <p className={mx('text-lg', !addedIdentity?.displayName && 'font-mono')}>
31
- {addedIdentity?.displayName ?? addedIdentity?.identityKey.truncate() ?? ' '}
65
+ <p className={mx('text-lg', !addedIdentity?.profile?.displayName && 'font-mono')}>
66
+ {addedIdentity?.profile?.displayName ?? addedIdentity?.identityKey.truncate() ?? ' '}
32
67
  </p>
33
68
  }
34
69
  variant='circle'
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
  };