feat(app): redesign discovery and align skeletons with rendered UI

This commit is contained in:
Hampus authored and GitHub committed 2026-08-16 19:17:28 +02:00
1 parent e054aa96be
commit 98c40c6979
380 files changed
+50869 -32207

No files matched your search

@@ -20,10 +20,20 @@ export const UserIdentityRow = observer(
const displayName = DisplayNameUtils.getNickname(user, guildId, channelId);
return (
<>
<Avatar user={user} size={avatarSize} guildId={guildId === null ? undefined : guildId} className={className} />
<flx-user-identity-row-info className={flxElementClassName(styles.info)}>
<span className={styles.name}>{displayName}</span>
<span className={styles.tag}>{user.tag}</span>
<Avatar
user={user}
size={avatarSize}
guildId={guildId === null ? undefined : guildId}
className={className}
data-flx="user.user-identity-row.avatar"
/>
<flx-user-identity-row-info className={flxElementClassName(styles.info)} data-flx="user.user-identity-row.info">
<span className={styles.name} data-flx="user.user-identity-row.name">
{displayName}
</span>
<span className={styles.tag} data-flx="user.user-identity-row.tag">
{user.tag}
</span>
</flx-user-identity-row-info>
</>
);
@@ -350,7 +350,11 @@ const LanguageTab = observer(() => {
data-flx="user.language-tab.settings-tab-section.language-settings"
>
<div className={styles.languageControls} data-flx="user.language-tab.language-controls">
<LanguageSelector value={currentLocale} onChange={handleLocaleChange} />
<LanguageSelector
value={currentLocale}
onChange={handleLocaleChange}
data-flx="user.language-tab.language-selector.locale-change"
/>
<div className={styles.notice} data-flx="user.language-tab.notice">
<p className={styles.noticeText} data-flx="user.language-tab.notice-text">
<Trans>
@@ -6,7 +6,7 @@ import {SettingsTabContainer, SettingsTabContent} from '@app/features/app/compon
import {StatusSlate} from '@app/features/app/components/dialogs/shared/StatusSlate';
import {computeVerticalDropPosition} from '@app/features/app/components/layout/dnd/DndDropPosition';
import type {ConnectionDragItem} from '@app/features/app/components/layout/types/DndTypes';
import {DND_TYPES} from '@app/features/app/components/layout/types/DndTypes';
import {DragItemType} from '@app/features/app/components/layout/types/DndTypes';
import {BLUESKY_PROVIDER_NAME, PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import {useMergeRefs} from '@app/features/app/hooks/useMergeRefs';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
@@ -85,7 +85,7 @@ interface ConnectionCardProps {
const ConnectionDragAutoScroll: React.FC<{listRef: React.RefObject<HTMLDivElement | null>}> = ({listRef}) => {
const isDraggingConnection = useDragLayer(
(monitor) => monitor.isDragging() && monitor.getItemType() === DND_TYPES.CONNECTION,
(monitor) => monitor.isDragging() && monitor.getItemType() === DragItemType.CONNECTION,
);
const getScrollElement = useCallback(() => {
const list = listRef.current;
@@ -103,7 +103,7 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
const cardRef = useRef<HTMLDivElement>(null);
const dragItemData = useMemo<ConnectionDragItem>(
() => ({
type: DND_TYPES.CONNECTION,
type: DragItemType.CONNECTION,
id: connection.id,
index,
}),
@@ -111,7 +111,7 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
);
const [{isDragging}, dragRef, preview] = useDrag(
() => ({
type: DND_TYPES.CONNECTION,
type: DragItemType.CONNECTION,
item: () => dragItemData,
collect: (monitor) => ({isDragging: monitor.isDragging()}),
end: () => setDropIndicator(null),
@@ -120,7 +120,7 @@ const ConnectionCard: React.FC<ConnectionCardProps> = observer(
);
const [{isOver}, dropRef] = useDrop(
() => ({
accept: DND_TYPES.CONNECTION,
accept: DragItemType.CONNECTION,
hover: (item: ConnectionDragItem, monitor) => {
if (item.id === connection.id) {
setDropIndicator(null);
@@ -431,7 +431,10 @@ const LinkedAccountsTab: React.FC = observer(() => {
</div>
) : (
<DndProvider backend={HTML5Backend} data-flx="user.linked-accounts-tab.dnd-provider">
<ConnectionDragAutoScroll listRef={connectionsListRef} />
<ConnectionDragAutoScroll
listRef={connectionsListRef}
data-flx="user.linked-accounts-tab.connection-drag-auto-scroll"
/>
<div
ref={connectionsListRef}
className={styles.connectionsList}
@@ -916,6 +916,7 @@ const MyProfileTabComponent = observer(function MyProfileTabComponent({
composerRef={bioComposerRef}
emojiPickerOpen={bioExpressionPickerOpen}
onEmojiPickerOpenChange={setBioExpressionPickerOpen}
data-flx="user.my-profile-tab.my-profile-tab-component.bio-editor.bio-change"
/>
</div>
</div>
@@ -251,7 +251,11 @@ export const ThemeTabContent: React.FC = observer(() => {
}, []);
return (
<>
<ThemeSelector value={currentSelectedTheme} onChange={handleThemeChange} />
<ThemeSelector
value={currentSelectedTheme}
onChange={handleThemeChange}
data-flx="user.appearance-tab.theme.theme-tab-content.theme-selector.theme-change"
/>
<Switch
label={i18n._(SYNC_THEME_ACROSS_DEVICES_DESCRIPTOR)}
description={i18n._(WHEN_ENABLED_THEME_CHANGES_WILL_SYNC_TO_ALL_DESCRIPTOR)}
@@ -123,8 +123,9 @@ export const BioEditor = observer(
aria-label={i18n._(OPEN_EMOJI_PICKER_DESCRIPTOR)}
aria-haspopup="dialog"
aria-expanded={emojiPickerOpen}
data-flx="user.my-profile-tab.bio-editor.emoji-button.emoji-picker-open-change"
>
<SmileyIcon size={remFromPx(20)} weight="fill" />
<SmileyIcon size={remFromPx(20)} weight="fill" data-flx="user.my-profile-tab.bio-editor.smiley-icon" />
</button>
</FocusRing>
) : (
@@ -147,10 +148,12 @@ export const BioEditor = observer(
}}
onClose={onClose}
visibleTabs={['emojis']}
data-flx="user.my-profile-tab.bio-editor.expression-picker-popout"
/>
)}
data-flx="user.my-profile-tab.bio-editor.popout"
>
<FocusRing offset={-2} enabled={!disabled}>
<FocusRing offset={-2} enabled={!disabled} data-flx="user.my-profile-tab.bio-editor.focus-ring">
<button
type="button"
className={clsx(styles.emojiButton, emojiPickerOpen && styles.emojiButtonActive)}
@@ -158,16 +161,17 @@ export const BioEditor = observer(
aria-label={i18n._(OPEN_EMOJI_PICKER_DESCRIPTOR)}
aria-haspopup="dialog"
aria-expanded={emojiPickerOpen}
data-flx="user.my-profile-tab.bio-editor.emoji-button"
>
<SmileyIcon size={remFromPx(20)} weight="fill" />
<SmileyIcon size={remFromPx(20)} weight="fill" data-flx="user.my-profile-tab.bio-editor.smiley-icon--2" />
</button>
</FocusRing>
</Popout>
);
return (
<div className="flx-element">
<fieldset className={formStyles.fieldset}>
<div className={formStyles.labelContainer}>
<div className="flx-element" data-flx="user.my-profile-tab.bio-editor.flx-element">
<fieldset className={formStyles.fieldset} data-flx="user.my-profile-tab.bio-editor.fieldset">
<div className={formStyles.labelContainer} data-flx="user.my-profile-tab.bio-editor.div">
<label
id={labelId}
htmlFor={editableId}
@@ -182,12 +186,20 @@ export const BioEditor = observer(
}
}}
className={formStyles.label}
data-flx="user.my-profile-tab.bio-editor.label.prevent-default"
>
{i18n._(ABOUT_ME_DESCRIPTOR)}
</label>
</div>
<div className={formStyles.inputGroup}>
<FocusRing within={true} ringTarget={wrapperRef} focusTarget={wrapperRef} offset={-2} enabled={!disabled}>
<div className={formStyles.inputGroup} data-flx="user.my-profile-tab.bio-editor.div--2">
<FocusRing
within={true}
ringTarget={wrapperRef}
focusTarget={wrapperRef}
offset={-2}
enabled={!disabled}
data-flx="user.my-profile-tab.bio-editor.focus-ring--2"
>
<div
ref={wrapperRef}
className={clsx(formStyles.textareaWrapper, surfaceStyles.surface, surfaceInteractionClass)}
@@ -205,6 +217,7 @@ export const BioEditor = observer(
composer.focus();
}
}}
data-flx="user.my-profile-tab.bio-editor.div.prevent-default"
>
<LexicalRichInput
key={hydrationKey}
@@ -231,29 +244,34 @@ export const BioEditor = observer(
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
i18n={i18n}
data-flx="user.my-profile-tab.bio-editor.editor.change"
/>
<div className={formStyles.textareaActions}>
<div className={formStyles.textareaActions} data-flx="user.my-profile-tab.bio-editor.div--3">
{emojiButton}
<div className={styles.characterCountContainer}>
<div
className={styles.characterCountContainer}
data-flx="user.my-profile-tab.bio-editor.character-count-container"
>
<CharacterCounter
currentLength={actualLength}
maxLength={actualMaxLength}
canUpgrade={false}
premiumMaxLength={actualMaxLength}
onUpgradeClick={() => undefined}
data-flx="user.my-profile-tab.bio-editor.character-counter"
/>
</div>
</div>
</div>
</FocusRing>
{hasError && (
<span id={errorId} className={formStyles.errorText}>
<span id={errorId} className={formStyles.errorText} data-flx="user.my-profile-tab.bio-editor.span">
{errorMessage}
</span>
)}
</div>
</fieldset>
<div id={descriptionId} className={styles.description}>
<div id={descriptionId} className={styles.description} data-flx="user.my-profile-tab.bio-editor.description">
<Trans>You can use links, emoji, and markdown.</Trans>
</div>
</div>
@@ -11,7 +11,7 @@ import {
import * as ContextMenuCommands from '@app/features/ui/commands/ContextMenuCommands';
import {StatusAwareAvatar} from '@app/features/ui/components/StatusAwareAvatar';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {getAppZoomFactor} from '@app/features/ui/utils/AppZoomUtils';
import {getAppRemScale} from '@app/features/ui/utils/AppZoomUtils';
import userProfileModalStyles from '@app/features/user/components/modals/UserProfileModal.module.css';
import {
isKeyboardContextMenuTrigger,
@@ -204,7 +204,7 @@ export const ProfileMediaHeader: React.FC<ProfileMediaHeaderProps> = observer(
);
const avatarContent = (
<StatusAwareAvatar
size={avatarSize / getAppZoomFactor()}
size={avatarSize / getAppRemScale()}
user={user}
avatarUrl={avatarUrl}
hoverAvatarUrl={hoverAvatarUrl}
@@ -3,14 +3,14 @@
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.75rem 0.875rem;
border-radius: var(--radius-md, 0.375rem);
gap: var(--active-now-card-gap);
padding: var(--active-now-card-padding-block) var(--active-now-card-padding-inline);
border-radius: var(--active-now-card-radius);
background-color: var(--background-secondary);
border: 0.0625rem solid var(--border-color, rgba(255, 255, 255, 0.04));
width: 100%;
align-self: stretch;
box-shadow: 0 0.375rem 1.125rem rgba(0, 0, 0, 0.08);
box-shadow: var(--active-now-card-shadow);
}
.headerContextGroup {
@@ -28,7 +28,7 @@
.headerLeft {
display: flex;
align-items: center;
gap: 0.375rem;
gap: var(--active-now-context-gap);
min-width: 0;
flex: 1;
}
@@ -48,7 +48,7 @@
.contextButton {
display: flex;
align-items: center;
gap: 0.375rem;
gap: var(--active-now-context-gap);
min-width: 0;
border: none;
padding: 0;
@@ -17,6 +17,10 @@ import {LiveBadge} from '@app/features/ui/components/LiveBadge';
import {Spinner} from '@app/features/ui/components/Spinner';
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import styles from '@app/features/user/components/profile/VoiceActivityCard.module.css';
import {
VOICE_ACTIVITY_AVATAR_MAX_VISIBLE,
VOICE_ACTIVITY_AVATAR_SIZE_PX,
} from '@app/features/user/components/profile/VoiceActivityCardMetrics';
import type {User} from '@app/features/user/models/User';
import Users from '@app/features/user/state/Users';
import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
@@ -252,8 +256,8 @@ export const VoiceActivityCard: React.FC<VoiceActivityCardProps> = observer(({ac
data-flx="user.profile.voice-activity-card.participants-row"
>
<AvatarStack
size={20}
maxVisible={5}
size={VOICE_ACTIVITY_AVATAR_SIZE_PX}
maxVisible={VOICE_ACTIVITY_AVATAR_MAX_VISIBLE}
users={avatarStackUsers}
guildId={guildId}
channelId={channelId}
@@ -0,0 +1,4 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
export const VOICE_ACTIVITY_AVATAR_SIZE_PX = 20;
export const VOICE_ACTIVITY_AVATAR_MAX_VISIBLE = 5;