From d9e2c3aa153e2c37f25ef01ceaaa3498a0c76aad Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Wed, 21 May 2025 09:49:04 +0100 Subject: [PATCH] Merge pull request #25755 from overleaf/mj-ide-collaborators-look [web] Align online user design to Figma GitOrigin-RevId: 89e09056558d98a57d3c1e5a8409476530784b26 --- .../web/frontend/extracted-translations.json | 2 + .../online-users/online-users-widget.tsx | 133 ++++++++++++++++++ .../components/toolbar/online-users.tsx | 2 +- .../frontend/stories/online-users.stories.tsx | 69 +++++++++ .../pages/editor/online-users.scss | 90 ++++++++++++ services/web/locales/en.json | 2 + 6 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx create mode 100644 services/web/frontend/stories/online-users.stories.tsx diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index d8f23bd45f..6e2205aed4 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx new file mode 100644 index 0000000000..bac1787e10 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx @@ -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 ( +
+ {usersBeforeOverflow.map((user, index) => ( + + ))} + {hasOverflow && ( + + )} +
+ ) +} + +const OnlineUserWidget = ({ + user, + goToUser, + id, +}: { + user: OnlineUser + goToUser: (user: OnlineUser) => void + id: string +}) => { + const onClick = useCallback(() => { + goToUser(user) + }, [goToUser, user]) + return ( + + + + ) +} + +const OnlineUserCircle = ({ user }: { user: OnlineUser }) => { + const backgroundColor = getBackgroundColorForUserId(user.user_id) + return ( + + {user.name.charAt(0)} + + ) +} + +const OnlineUserOverflow = ({ + goToUser, + users, +}: { + goToUser: (user: OnlineUser) => void + users: OnlineUser[] +}) => { + const { t } = useTranslation() + return ( + + + + +{users.length} + + + + + {users.map((user, index) => ( +
  • + goToUser(user)} + > + {user.name} + +
  • + ))} +
    +
    + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx b/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx index d8fb314907..4d937fa89c 100644 --- a/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/toolbar/online-users.tsx @@ -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() diff --git a/services/web/frontend/stories/online-users.stories.tsx b/services/web/frontend/stories/online-users.stories.tsx new file mode 100644 index 0000000000..7d2796cb2e --- /dev/null +++ b/services/web/frontend/stories/online-users.stories.tsx @@ -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 ( +
    + {}} /> +
    + ) +} + +export const OnlineUsersOld = ({ users }: { users: number }) => { + const generatedUsers = Array.from({ length: users }, generateUser) + return ( +
    + {}} /> +
    + ) +} + +const meta: Meta = { + title: 'Editor / Online Users Widget', + args: { + users: 6, + }, +} + +export default meta diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss index 52265c6e97..2a03c22c19 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss @@ -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; + } +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 08f22483a7..99f4f27473 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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",