fix(ui): resolve 29 native UI/UX inconsistencies from folds design audit
CI / Build & Quality Checks (push) Successful in 10m25s
CI / Trigger Desktop Build (push) Successful in 6s

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:
2026-06-18 22:46:19 -04:00
parent 9742eaea28
commit 8dc4c4d072
21 changed files with 757 additions and 467 deletions
@@ -25,8 +25,8 @@ import {
Input,
Badge,
RectCords,
color,
} from 'folds';
import { color } from 'folds';
import { SearchOrderBy } from 'matrix-js-sdk';
import FocusTrap from 'focus-trap-react';
import { useVirtualizer } from '@tanstack/react-virtual';
@@ -374,7 +374,10 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro
const searchUser = useDebounce(_searchUser, SEARCH_DEBOUNCE_OPTS);
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
const value = evt.currentTarget.value.trim();
if (!value) { resetSearch(); return; }
if (!value) {
resetSearch();
return;
}
searchUser(value);
};
@@ -419,14 +422,30 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro
>
<Menu variant="Surface" style={{ width: toRem(250) }}>
<Box direction="Column" style={{ maxHeight: toRem(400), maxWidth: toRem(300) }}>
<Box shrink="No" direction="Column" gap="100" style={{ padding: config.space.S200, paddingBottom: 0 }}>
<Box
shrink="No"
direction="Column"
gap="100"
style={{ padding: config.space.S200, paddingBottom: 0 }}
>
<Text size="L400">From</Text>
<Input onChange={handleSearchChange} size="300" radii="300" placeholder="Search people..." />
<Input
onChange={handleSearchChange}
size="300"
radii="300"
placeholder="Search people..."
/>
</Box>
<Scroll ref={scrollRef} size="300" hideTrack>
<Box direction="Column" gap="100" style={{ padding: config.space.S200, paddingRight: 0 }}>
<Box
direction="Column"
gap="100"
style={{ padding: config.space.S200, paddingRight: 0 }}
>
{users.length === 0 && (
<Text style={{ padding: config.space.S400 }} size="T300" align="Center">No match found!</Text>
<Text style={{ padding: config.space.S400 }} size="T300" align="Center">
No match found!
</Text>
)}
<div style={{ position: 'relative', height: virtualizer.getTotalSize() }}>
{vItems.map((vItem) => {
@@ -450,7 +469,9 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro
aria-pressed={selected}
before={<Icon size="50" src={Icons.User} />}
>
<Text truncate size="T300">{name}</Text>
<Text truncate size="T300">
{name}
</Text>
</MenuItem>
</VirtualTile>
);
@@ -467,7 +488,14 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro
<Text size="B300">Save</Text>
)}
</Button>
<Button size="300" radii="300" variant="Secondary" fill="Soft" onClick={handleDeselectAll} disabled={!localSelected || localSelected.length === 0}>
<Button
size="300"
radii="300"
variant="Secondary"
fill="Soft"
onClick={handleDeselectAll}
disabled={!localSelected || localSelected.length === 0}
>
<Text size="B300">Deselect All</Text>
</Button>
</Box>
@@ -477,7 +505,9 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro
}
>
<Chip
onClick={(e: React.MouseEvent<HTMLButtonElement>) => setMenuAnchor(e.currentTarget.getBoundingClientRect())}
onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
setMenuAnchor(e.currentTarget.getBoundingClientRect())
}
variant="SurfaceVariant"
radii="Pill"
before={<Icon size="100" src={Icons.User} />}
@@ -529,22 +559,28 @@ function DateRangeButton({ fromTs, toTs, onChange }: DateRangeButtonProps) {
<Box direction="Column" gap="100">
<Text size="L400">Quick pick</Text>
<Box gap="100" wrap="Wrap">
{([
{ label: 'Today', days: 0 },
{ label: 'Last week', days: 7 },
{ label: 'Last month', days: 30 },
{ label: 'Last year', days: 365 },
] as const).map(({ label: l, days }) => {
{(
[
{ label: 'Today', days: 0 },
{ label: 'Last week', days: 7 },
{ label: 'Last month', days: 30 },
{ label: 'Last year', days: 365 },
] as const
).map(({ label: l, days }) => {
const now = Date.now();
const from = days === 0
? new Date().setHours(0, 0, 0, 0)
: now - days * 24 * 60 * 60 * 1000;
const from =
days === 0
? new Date().setHours(0, 0, 0, 0)
: now - days * 24 * 60 * 60 * 1000;
return (
<Chip
key={l}
radii="Pill"
variant="SurfaceVariant"
onClick={() => { onChange(from, now); setMenuAnchor(undefined); }}
onClick={() => {
onChange(from, now);
setMenuAnchor(undefined);
}}
>
<Text size="T200">{l}</Text>
</Chip>
@@ -746,13 +782,11 @@ export function SearchFilters({
</Chip>
);
})}
<SelectSenderButton
selectedSenders={selectedSenders}
onChange={onSelectedSendersChange}
/>
<SelectSenderButton selectedSenders={selectedSenders} onChange={onSelectedSendersChange} />
<Box grow="Yes" data-spacing-node />
<Chip
variant={containsUrl ? 'Primary' : 'SurfaceVariant'}
variant={containsUrl ? 'Success' : 'SurfaceVariant'}
outlined={!!containsUrl}
radii="Pill"
aria-pressed={!!containsUrl}
before={<Icon size="100" src={Icons.Link} />}
@@ -761,7 +795,10 @@ export function SearchFilters({
<Icon
size="50"
src={Icons.Cross}
onClick={(e) => { e.stopPropagation(); onContainsUrlChange(undefined); }}
onClick={(e) => {
e.stopPropagation();
onContainsUrlChange(undefined);
}}
/>
) : undefined
}