mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-25 10:10:08 +02:00
Merge pull request #25755 from overleaf/mj-ide-collaborators-look
[web] Align online user design to Figma GitOrigin-RevId: 89e09056558d98a57d3c1e5a8409476530784b26
This commit is contained in:
committed by
Copybot
parent
b1daf50765
commit
d9e2c3aa15
@@ -1042,6 +1042,8 @@
|
||||
"my_library": "",
|
||||
"n_items": "",
|
||||
"n_items_plural": "",
|
||||
"n_more_collaborators": "",
|
||||
"n_more_collaborators_plural": "",
|
||||
"n_more_updates_above": "",
|
||||
"n_more_updates_above_plural": "",
|
||||
"n_more_updates_below": "",
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { OnlineUser } from '@/features/ide-react/context/online-users-context'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownHeader,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/features/ui/components/bootstrap-5/dropdown-menu'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import { getBackgroundColorForUserId } from '@/shared/utils/colors'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// Should be kept in sync with $max-user-circles-displayed CSS constant
|
||||
const MAX_USER_CIRCLES_DISPLAYED = 5
|
||||
|
||||
// We don't want a +1 circle since we could just show the user instead
|
||||
const MAX_USERS_WITH_OVERFLOW_VISIBLE = MAX_USER_CIRCLES_DISPLAYED - 1
|
||||
|
||||
export const OnlineUsersWidget = ({
|
||||
onlineUsers,
|
||||
goToUser,
|
||||
}: {
|
||||
onlineUsers: OnlineUser[]
|
||||
goToUser: (user: OnlineUser) => void
|
||||
}) => {
|
||||
const hasOverflow = onlineUsers.length > MAX_USER_CIRCLES_DISPLAYED
|
||||
const usersBeforeOverflow = useMemo(
|
||||
() =>
|
||||
hasOverflow
|
||||
? onlineUsers.slice(0, MAX_USERS_WITH_OVERFLOW_VISIBLE)
|
||||
: onlineUsers,
|
||||
[onlineUsers, hasOverflow]
|
||||
)
|
||||
const usersInOverflow = useMemo(
|
||||
() =>
|
||||
hasOverflow ? onlineUsers.slice(MAX_USERS_WITH_OVERFLOW_VISIBLE) : [],
|
||||
[onlineUsers, hasOverflow]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="online-users-row">
|
||||
{usersBeforeOverflow.map((user, index) => (
|
||||
<OnlineUserWidget
|
||||
key={`${user.user_id}_${index}`}
|
||||
user={user}
|
||||
goToUser={goToUser}
|
||||
id={`online-user-${user.user_id}_${index}`}
|
||||
/>
|
||||
))}
|
||||
{hasOverflow && (
|
||||
<OnlineUserOverflow goToUser={goToUser} users={usersInOverflow} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OnlineUserWidget = ({
|
||||
user,
|
||||
goToUser,
|
||||
id,
|
||||
}: {
|
||||
user: OnlineUser
|
||||
goToUser: (user: OnlineUser) => void
|
||||
id: string
|
||||
}) => {
|
||||
const onClick = useCallback(() => {
|
||||
goToUser(user)
|
||||
}, [goToUser, user])
|
||||
return (
|
||||
<OLTooltip
|
||||
id={id}
|
||||
description={user.name}
|
||||
overlayProps={{
|
||||
placement: 'bottom',
|
||||
trigger: ['hover', 'focus'],
|
||||
delay: 0,
|
||||
}}
|
||||
>
|
||||
<button className="online-users-row-button" onClick={onClick}>
|
||||
<OnlineUserCircle user={user} />
|
||||
</button>
|
||||
</OLTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const OnlineUserCircle = ({ user }: { user: OnlineUser }) => {
|
||||
const backgroundColor = getBackgroundColorForUserId(user.user_id)
|
||||
return (
|
||||
<span className="online-user-circle" style={{ backgroundColor }}>
|
||||
{user.name.charAt(0)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const OnlineUserOverflow = ({
|
||||
goToUser,
|
||||
users,
|
||||
}: {
|
||||
goToUser: (user: OnlineUser) => void
|
||||
users: OnlineUser[]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Dropdown align="end">
|
||||
<DropdownToggle className="online-users-row-button online-user-overflow-toggle">
|
||||
<OLTooltip
|
||||
id="connected-users"
|
||||
description={t('n_more_collaborators', { count: users.length })}
|
||||
overlayProps={{ placement: 'bottom' }}
|
||||
>
|
||||
<span className="online-user-circle">+{users.length}</span>
|
||||
</OLTooltip>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu className="online-user-overflow-dropdown">
|
||||
<DropdownHeader aria-hidden="true">
|
||||
{t('connected_users')}
|
||||
</DropdownHeader>
|
||||
{users.map((user, index) => (
|
||||
<li role="none" key={`${user.user_id}_${index}`}>
|
||||
<DropdownItem
|
||||
as="button"
|
||||
tabIndex={-1}
|
||||
onClick={() => goToUser(user)}
|
||||
>
|
||||
<OnlineUserCircle user={user} /> {user.name}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import OnlineUsersWidget from '@/features/editor-navigation-toolbar/components/online-users-widget'
|
||||
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
||||
import {
|
||||
OnlineUser,
|
||||
useOnlineUsersContext,
|
||||
} from '@/features/ide-react/context/online-users-context'
|
||||
import { useCallback } from 'react'
|
||||
import { OnlineUsersWidget } from '../online-users/online-users-widget'
|
||||
|
||||
export const OnlineUsers = () => {
|
||||
const { openDoc } = useEditorManagerContext()
|
||||
|
||||
69
services/web/frontend/stories/online-users.stories.tsx
Normal file
69
services/web/frontend/stories/online-users.stories.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Meta } from '@storybook/react'
|
||||
import { OnlineUser } from '@/features/ide-react/context/online-users-context'
|
||||
import OnlineUsersWidgetOld from '@/features/editor-navigation-toolbar/components/online-users-widget'
|
||||
import { OnlineUsersWidget } from '@/features/ide-redesign/components/online-users/online-users-widget'
|
||||
|
||||
const NAMES = [
|
||||
'Alice',
|
||||
'Bob',
|
||||
'Charlie',
|
||||
'Dave',
|
||||
'Erin',
|
||||
'Frank',
|
||||
'Grace',
|
||||
'Heidi',
|
||||
'Ivan',
|
||||
'Judy',
|
||||
'Mallory',
|
||||
'Niaj',
|
||||
'Olivia',
|
||||
'Peggy',
|
||||
'Rupert',
|
||||
]
|
||||
|
||||
const generateUser = (_: any, index: number): OnlineUser => {
|
||||
const name = NAMES[index % NAMES.length]
|
||||
return {
|
||||
user_id: `user_${'b'.repeat(index)}`,
|
||||
name,
|
||||
id: `user-${index}`,
|
||||
email: `${name.toLowerCase()}@example.com`,
|
||||
}
|
||||
}
|
||||
|
||||
export const OnlineUsersRedesign = ({ users }: { users: number }) => {
|
||||
const generatedUsers = Array.from({ length: users }, generateUser)
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--online-users-border-color)',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<OnlineUsersWidget onlineUsers={generatedUsers} goToUser={() => {}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const OnlineUsersOld = ({ users }: { users: number }) => {
|
||||
const generatedUsers = Array.from({ length: users }, generateUser)
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--online-users-border-color)',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<OnlineUsersWidgetOld onlineUsers={generatedUsers} goToUser={() => {}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const meta: Meta<typeof OnlineUsersRedesign> = {
|
||||
title: 'Editor / Online Users Widget',
|
||||
args: {
|
||||
users: 6,
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
||||
@@ -1,9 +1,15 @@
|
||||
:root {
|
||||
--toolbar-btn-color: var(--white);
|
||||
--online-users-border-color: var(--bg-dark-primary);
|
||||
--online-users-text-color: var(--content-primary);
|
||||
--online-users-overflow-button-background-color: var(--bg-light-secondary);
|
||||
}
|
||||
|
||||
@include theme('light') {
|
||||
--toolbar-btn-color: var(--neutral-70);
|
||||
--online-users-border-color: var(--bg-light-primary);
|
||||
--online-users-text-color: var(--content-primary);
|
||||
--online-users-overflow-button-background-color: var(--bg-light-secondary);
|
||||
}
|
||||
|
||||
.online-users {
|
||||
@@ -35,3 +41,87 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.online-users-row {
|
||||
// Keep in sync with the MAX_USER_CIRCLES_DISPLAYED js constant
|
||||
$max-user-circles-displayed: 5;
|
||||
|
||||
--online-users-circle-size: 24px;
|
||||
--online-users-border-size: 1px;
|
||||
--online-users-overlap: 8px;
|
||||
--online-users-circle-padding: var(--spacing-01);
|
||||
|
||||
.online-user-overflow-dropdown {
|
||||
--online-users-border-color: transparent;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--online-users-text-color);
|
||||
|
||||
.online-users-row-button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--online-users-text-color);
|
||||
min-width: var(--online-users-circle-size);
|
||||
height: var(--online-users-circle-size);
|
||||
display: block;
|
||||
font-weight: var(--font-weight-regular);
|
||||
|
||||
&.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:not(:last-child, .online-user-overflow-toggle) {
|
||||
margin-right: calc(
|
||||
var(--online-users-overlap) * -1 + var(--online-users-border-size)
|
||||
);
|
||||
}
|
||||
|
||||
@for $i from 1 through $max-user-circles-displayed {
|
||||
&:nth-child(#{$i}):not(:hover):not(.online-user-overflow-toggle) {
|
||||
z-index: $i;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: $max-user-circles-displayed + 2;
|
||||
}
|
||||
}
|
||||
|
||||
.online-user-overflow-toggle {
|
||||
&:not(:hover) {
|
||||
z-index: $max-user-circles-displayed + 1;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
& {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.online-user-circle {
|
||||
background-color: var(--online-users-overflow-button-background-color);
|
||||
color: var(--online-users-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.online-user-circle {
|
||||
padding: var(--online-users-circle-padding);
|
||||
border-radius: 50%;
|
||||
min-width: var(--online-users-circle-size);
|
||||
height: var(--online-users-circle-size);
|
||||
line-height: calc(
|
||||
var(--online-users-circle-size) - 2 * var(--online-users-border-size) - 2 *
|
||||
var(--online-users-circle-padding)
|
||||
);
|
||||
font-size: var(--font-size-01);
|
||||
text-align: center;
|
||||
border: var(--online-users-border-size) solid
|
||||
var(--online-users-border-color);
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1365,6 +1365,8 @@
|
||||
"my_library": "My Library",
|
||||
"n_items": "__count__ item",
|
||||
"n_items_plural": "__count__ items",
|
||||
"n_more_collaborators": "__count__ more collaborator",
|
||||
"n_more_collaborators_plural": "__count__ more collaborators",
|
||||
"n_more_updates_above": "__count__ more update above",
|
||||
"n_more_updates_above_plural": "__count__ more updates above",
|
||||
"n_more_updates_below": "__count__ more update below",
|
||||
|
||||
Reference in New Issue
Block a user