fix(ui): resolve 29 native UI/UX inconsistencies from folds design audit
Fixes N1–N94 findings from LOTUS_BUGS.md audit pass. Key changes: - ProfileDecoration: raw <button> → folds <Button> for save/remove; remove undefined --accent-cyan var - UserRoomProfile: textarea border uses color.SurfaceVariant.ContainerLine and config tokens instead of undefined --border-interactive var - LotusToastContainer: z-index raised from 9997 → 10001 so toasts appear above Night Light overlay (9998) and modals (9999) - Message.tsx: DeliveryStatus replaces Unicode glyphs with Icon components; MessageQuickReactions returns null instead of <span />; forward menu item gets correct size="100" on after icon - AudioContent: speed chip variant/radii now matches Play chip (Secondary/300) - ReadReceiptAvatars: pill border/radius/padding → folds config tokens; remove dead receipt-pill-btn className - EventReaders: Header size 600→500; close button gets radii="300"; borderBottom shorthand → borderBottomWidth token; remove raw fontSize - General.tsx: selected background/seasonal picker border uses color.Primary.Main instead of color.Critical.Main (error red) - RoomInsights: SectionHeader drops textTransform/letterSpacing/opacity; chart borderRadius → config tokens; remove raw fontSize:9; warning banner → SequenceCard - RoomProfile.tsx: formatting toolbar raw <button> → folds <Button>; topic read-mode renders formatted_body via sanitizeCustomHtml - MsgTypeRenderers: location Open button Chip→Button; opacity:0.65→priority - UploadCardRenderer: caption raw <input> → folds <Input> - VoiceMessageRecorder: replace undefined --bg-surface-variant/--tc-* vars with color.* tokens; replace bare <audio controls> with IconButton play/pause toggle - App.tsx: mention highlight uses WCAG 2.1 relative luminance (gamma linearization) instead of simplified approximation; border now rgba semi-transparent instead of same color as background - RoomNavItem: Mute MenuItem icon moved to before prop - SearchFilters: HasLink chip variant="Success" outlined to match filter bar - RoomViewHeader: Server Notice chip radii Pill→300; fix jotai import order - Fix ESLint import/order errors in DeviceVerificationSetup, RoomTopicViewer, MediaGallery, and RoomViewHeader Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import React, { FormEventHandler, forwardRef, useCallback, useState } from 'react';
|
||||
import { useModalStyle } from '../hooks/useModalStyle';
|
||||
import {
|
||||
Dialog,
|
||||
Header,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import FileSaver from 'file-saver';
|
||||
import to from 'await-to-js';
|
||||
import { AuthDict, IAuthData, MatrixError, UIAuthCallback } from 'matrix-js-sdk';
|
||||
import { useModalStyle } from '../hooks/useModalStyle';
|
||||
import { PasswordInput } from './password-input';
|
||||
import { ContainerColor } from '../styles/ContainerColor.css';
|
||||
import { copyToClipboard } from '../utils/dom';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Icon, IconButton, Icons, Text, config, toRem } from 'folds';
|
||||
import { Box, Icon, IconButton, Icons, Text, color, config, toRem } from 'folds';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
|
||||
@@ -51,6 +51,8 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
||||
|
||||
const previewMimeRef = useRef('audio/ogg;codecs=opus');
|
||||
const previewDurationRef = useRef(0);
|
||||
const previewAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const [previewPlaying, setPreviewPlaying] = useState(false);
|
||||
|
||||
const stopAll = useCallback(() => {
|
||||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||||
@@ -192,7 +194,7 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
borderRadius: config.radii.R300,
|
||||
padding: `${toRem(4)} ${toRem(8)}`,
|
||||
}}
|
||||
@@ -203,7 +205,7 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
||||
width: toRem(8),
|
||||
height: toRem(8),
|
||||
borderRadius: '50%',
|
||||
background: lotusTerminal ? 'var(--lt-accent-orange)' : 'var(--tc-danger-normal)',
|
||||
background: lotusTerminal ? 'var(--lt-accent-orange)' : color.Critical.Main,
|
||||
flexShrink: 0,
|
||||
animation: 'pttLivePulse 900ms ease-in-out infinite',
|
||||
}}
|
||||
@@ -237,7 +239,7 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
||||
width: toRem(2),
|
||||
height: toRem(2 + (h / barMax) * 16),
|
||||
borderRadius: toRem(1),
|
||||
background: lotusTerminal ? 'var(--lt-accent-green)' : 'var(--tc-primary-normal)',
|
||||
background: lotusTerminal ? 'var(--lt-accent-green)' : color.Primary.Main,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
@@ -273,13 +275,36 @@ export function VoiceMessageRecorder({ onSend, onError }: VoiceRecorderProps) {
|
||||
alignItems="Center"
|
||||
gap="200"
|
||||
style={{
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
borderRadius: config.radii.R300,
|
||||
padding: `${toRem(4)} ${toRem(8)}`,
|
||||
}}
|
||||
>
|
||||
{previewUrl && (
|
||||
<audio src={previewUrl} controls style={{ height: toRem(28), maxWidth: toRem(180) }} />
|
||||
<>
|
||||
<audio ref={previewAudioRef} src={previewUrl} onEnded={() => setPreviewPlaying(false)} />
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
const audio = previewAudioRef.current;
|
||||
if (!audio) return;
|
||||
if (previewPlaying) {
|
||||
audio.pause();
|
||||
setPreviewPlaying(false);
|
||||
} else {
|
||||
audio.play();
|
||||
setPreviewPlaying(true);
|
||||
}
|
||||
}}
|
||||
aria-label={previewPlaying ? 'Pause preview' : 'Play preview'}
|
||||
variant="Secondary"
|
||||
fill="Soft"
|
||||
size="300"
|
||||
radii="300"
|
||||
title={previewPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
<Icon src={previewPlaying ? Icons.Pause : Icons.Play} size="100" />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
<Text size="T200" style={{ fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
|
||||
{formatDuration(previewDurationRef.current)}
|
||||
|
||||
@@ -67,11 +67,11 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
<Header
|
||||
className={css.Header}
|
||||
variant="Surface"
|
||||
size="600"
|
||||
size="500"
|
||||
style={
|
||||
lotusTerminal
|
||||
? {
|
||||
borderBottom: '1px solid var(--lt-border-color)',
|
||||
borderBottomWidth: config.borderWidth.B300,
|
||||
boxShadow: 'var(--lt-box-glow-cyan)',
|
||||
}
|
||||
: undefined
|
||||
@@ -93,7 +93,7 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
Seen by
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={requestClose} aria-label="Close">
|
||||
<IconButton size="300" radii="300" onClick={requestClose} aria-label="Close">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
@@ -141,14 +141,14 @@ export const EventReaders = as<'div', EventReadersProps>(
|
||||
{receiptTs !== undefined && (
|
||||
<Text
|
||||
size="T200"
|
||||
priority="300"
|
||||
style={
|
||||
lotusTerminal
|
||||
? {
|
||||
color: 'var(--lt-accent-amber)',
|
||||
textShadow: 'var(--lt-glow-amber)',
|
||||
fontSize: '0.72rem',
|
||||
}
|
||||
: { opacity: 0.6 }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{formatReadTs(receiptTs, hour24Clock)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Button, Chip, Icon, Icons, Text, color, toRem } from 'folds';
|
||||
import { Box, Button, Icon, Icons, Text, color, toRem } from 'folds';
|
||||
import { IContent } from 'matrix-js-sdk';
|
||||
import { JUMBO_EMOJI_REG, URL_REG } from '../../utils/regex';
|
||||
import { trimReplyFromBody } from '../../utils/room';
|
||||
@@ -529,21 +529,22 @@ export function MLocation({ content }: MLocationProps) {
|
||||
loading="lazy"
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
<Text size="T300" style={{ opacity: 0.65 }}>
|
||||
<Text size="T300" priority="300">
|
||||
{`${lat.toFixed(5)}, ${lon.toFixed(5)}`}
|
||||
</Text>
|
||||
<Chip
|
||||
<Button
|
||||
as="a"
|
||||
size="400"
|
||||
href={`https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=16/${location.latitude}/${location.longitude}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
variant="Secondary"
|
||||
fill="Solid"
|
||||
radii="300"
|
||||
before={<Icon src={Icons.External} size="50" />}
|
||||
>
|
||||
<Text size="B300">Open Location</Text>
|
||||
</Chip>
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ export function AudioContent({
|
||||
|
||||
<Chip
|
||||
onClick={handleSpeedClick}
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
variant="Secondary"
|
||||
radii="300"
|
||||
aria-label={`Playback speed: ${playbackSpeed}×`}
|
||||
>
|
||||
<Text size="B300">{`${playbackSpeed}×`}</Text>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Text, color } from 'folds';
|
||||
import {
|
||||
Icon,
|
||||
Icons,
|
||||
Modal,
|
||||
Overlay,
|
||||
OverlayBackdrop,
|
||||
OverlayCenter,
|
||||
Text,
|
||||
color,
|
||||
config,
|
||||
} from 'folds';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
@@ -64,7 +74,6 @@ export function ReadReceiptAvatars({
|
||||
onClick={() => setOpen(true)}
|
||||
title={tooltipNames}
|
||||
aria-label={tooltipNames}
|
||||
className="receipt-pill-btn"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
@@ -95,10 +104,12 @@ export function ReadReceiptAvatars({
|
||||
backgroundColor: lotusTerminal
|
||||
? 'rgba(0,212,255,0.07)'
|
||||
: color.SurfaceVariant.Container,
|
||||
border: lotusTerminal ? '1px solid rgba(0,212,255,0.30)' : '1px solid transparent',
|
||||
border: lotusTerminal
|
||||
? `${config.borderWidth.B300} solid rgba(0,212,255,0.30)`
|
||||
: `${config.borderWidth.B300} solid transparent`,
|
||||
boxShadow: lotusTerminal ? '0 0 10px rgba(0,212,255,0.12)' : 'none',
|
||||
borderRadius: '999px',
|
||||
padding: '2px 6px',
|
||||
borderRadius: config.radii.Pill,
|
||||
padding: `${config.space.S100} ${config.space.S200}`,
|
||||
gap: '0px',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import parse from 'html-react-parser';
|
||||
import { as, Box, Header, Icon, IconButton, Icons, Modal, Scroll, Text } from 'folds';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
import classNames from 'classnames';
|
||||
import Linkify from 'linkify-react';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
import * as css from './style.css';
|
||||
import { LINKIFY_OPTS, scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
|
||||
import { sanitizeCustomHtml } from '../../utils/sanitize';
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import React, { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Chip, Icon, IconButton, Icons, Switch, Text, color, config, toRem } from 'folds';
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Switch,
|
||||
Text,
|
||||
color,
|
||||
config,
|
||||
toRem,
|
||||
} from 'folds';
|
||||
import { UploadCard, UploadCardError, UploadCardProgress } from './UploadCard';
|
||||
import { UploadStatus, UploadSuccess, useBindUploadAtom } from '../../state/upload';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
@@ -353,25 +365,18 @@ export function UploadCardRenderer({
|
||||
)}
|
||||
{(fileItem.originalFile.type.startsWith('image') ||
|
||||
fileItem.originalFile.type.startsWith('video')) && (
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Add a caption… (optional)"
|
||||
value={metadata.caption ?? ''}
|
||||
onChange={(e) => setMetadata(fileItem, { ...metadata, caption: e.target.value })}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setMetadata(fileItem, { ...metadata, caption: e.target.value })
|
||||
}
|
||||
data-caption-input
|
||||
style={{
|
||||
marginTop: '6px',
|
||||
width: '100%',
|
||||
background: 'var(--bg-surface-low)',
|
||||
border: '1px solid var(--bg-surface-border)',
|
||||
borderRadius: '6px',
|
||||
padding: '5px 8px',
|
||||
fontSize: '0.85rem',
|
||||
color: 'var(--text-primary)',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
}}
|
||||
variant="Secondary"
|
||||
size="300"
|
||||
radii="300"
|
||||
style={{ marginTop: config.space.S200, width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
<CompressionCheckbox fileItem={fileItem} metadata={metadata} setMetadata={setMetadata} />
|
||||
|
||||
@@ -89,7 +89,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) {
|
||||
return (
|
||||
<DeviceVerificationStatus crypto={crypto} userId={userId} deviceId={device.deviceId}>
|
||||
{(status) => {
|
||||
const color =
|
||||
const deviceColor =
|
||||
status === VerificationStatus.Verified
|
||||
? 'var(--tc-positive-normal, #5effc4)'
|
||||
: status === VerificationStatus.Unverified
|
||||
@@ -97,7 +97,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) {
|
||||
: 'var(--tc-surface-low-contrast)';
|
||||
return (
|
||||
<Box alignItems="Center" gap="200">
|
||||
<Icon size="100" src={Icons.ShieldUser} style={{ color, flexShrink: 0 }} />
|
||||
<Icon size="100" src={Icons.ShieldUser} style={{ color: deviceColor, flexShrink: 0 }} />
|
||||
<Box grow="Yes" direction="Column" style={{ minWidth: 0 }}>
|
||||
<Text size="T300" truncate>
|
||||
{device.displayName ?? device.deviceId}
|
||||
@@ -239,7 +239,7 @@ function UserPrivateNotes({ userId }: { userId: string }) {
|
||||
<Box direction="Column" gap="200">
|
||||
<Box justifyContent="SpaceBetween" alignItems="Center">
|
||||
<Text size="L400">Private Note</Text>
|
||||
<Text size="T200" style={{ opacity: 0.5 }}>
|
||||
<Text size="T200" priority="400">
|
||||
{saving ? 'Saving…' : charsLeft < 100 ? `${charsLeft} left` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -252,12 +252,11 @@ function UserPrivateNotes({ userId }: { userId: string }) {
|
||||
style={{
|
||||
width: '100%',
|
||||
resize: 'vertical',
|
||||
background: 'var(--bg-surface-variant)',
|
||||
background: color.SurfaceVariant.Container,
|
||||
color: 'inherit',
|
||||
border: '1px solid var(--border-interactive)',
|
||||
borderRadius: '6px',
|
||||
padding: '8px 10px',
|
||||
fontSize: '14px',
|
||||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
borderRadius: config.radii.R300,
|
||||
padding: `${config.space.S200} ${config.space.S300}`,
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: 1.5,
|
||||
boxSizing: 'border-box',
|
||||
|
||||
Reference in New Issue
Block a user