diff --git a/LOTUS_BUGS.md b/LOTUS_BUGS.md index 831661f1c..0c6e8af38 100644 --- a/LOTUS_BUGS.md +++ b/LOTUS_BUGS.md @@ -129,23 +129,23 @@ This document tracks identified bugs, edge cases, and architectural discrepancie ## πŸ” Technical & Performance Refinements -| Category | Issue Description | File Path | Status | -| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | :----- | -| State Sync | Fire-and-forget network call to set offline presence during `pagehide` event may not complete reliably, potentially causing UI drift in presence status. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN | -| State Sync | Fire-and-forget network call `setPresence().catch(...)` suppresses errors, meaning the app may falsely assume presence update success. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN | +| Category | Issue Description | File Path | Status | +| :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| State Sync | Fire-and-forget network call to set offline presence during `pagehide` event may not complete reliably, potentially causing UI drift in presence status. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN | +| State Sync | Fire-and-forget network call `setPresence().catch(...)` suppresses errors, meaning the app may falsely assume presence update success. | `cinny/src/app/hooks/usePresenceUpdater.ts` | OPEN | | Memory Leak | Decrypted Media Memory Leak (Gallery & Lightbox) due to missing virtualization and blob revocation. | `cinny/src/app/features/room/MediaGallery.tsx` | PARTIALLY FIXED ⚠️ UNTESTED β€” Blob revocation was already correct; added `enabled` param to `useDecryptedMediaUrl` and `useNearViewport(300px)` to each `GalleryTile` to gate decryption until near-viewport, reducing burst on pagination. True virtualization (windowing) deferred β€” requires significant refactor. | -| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | FIXED β€” now uses `atomWithStorage` + `createJSONStorage` (Jotai's built-in persistence with error-safe JSON parsing) | -| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | FALSE POSITIVE β€” `usePan` already uses `attachedRef` to track listeners and cleans them up in an unmount `useEffect`. No change needed. | -| Asset Optimization | Large unoptimized media asset (213KB) found in `public/res`. | `public/res/Lotus.png` | OPEN | -| Data Persistence | Non-atomic `localStorage` updates in session management can lead to inconsistent state. | `cinny/src/app/state/sessions.ts` | OPEN | -| Data Persistence | Lack of cross-tab synchronization for `localStorage` updates in session management risks race conditions. | `cinny/src/app/state/sessions.ts` | OPEN | -| Network Resilience | `uploadContent` lacks retry logic, failing immediately upon network error. | `cinny/src/app/utils/matrix.ts` | OPEN | -| Network Resilience | `rateLimitedActions` uses basic retry logic without exponential backoff, which may exacerbate 429 issues. | `cinny/src/app/utils/matrix.ts` | FIXED β€” fallback delay now uses capped exponential backoff (`min(1000 * 2^retryCount, 30_000)ms`) when server doesn't send `Retry-After`; server header still takes precedence via `getRetryAfterMs()`. | -| Matrix Event Robustness | `useMatrixEventRenderer` handles unknown events gracefully by returning `null`, which may hide potentially important unrendered data. | `cinny/src/app/hooks/useMatrixEventRenderer.ts` | OPEN | -| Data Contract | `MatrixError` instantiation with `UploadResponse` might be brittle. | `cinny/src/app/utils/matrix.ts` | OPEN | -| Type Safety | `addRoomIdToMDirect` uses `as any` cast for `AccountDataEvent.Direct`, bypassing type contract validation. | `cinny/src/app/utils/matrix.ts` | OPEN | -| Robustness | `rateLimitedActions` relies on `MatrixError.httpStatus` which might not exist on all error variants. | `cinny/src/app/utils/matrix.ts` | FALSE POSITIVE β€” `MatrixError.httpStatus` is defined as `readonly httpStatus?: number` in `matrix-js-sdk/lib/http-api/errors.d.ts`. It is optional (not on all instances) but the `?.` optional chain already guards against undefined. No change needed. | -| Type Contract | Custom types in `cinny/src/types/matrix` mirror SDK types instead of using them, risking drift and contract mismatches. | `cinny/src/types/matrix/` | OPEN | +| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | FIXED β€” now uses `atomWithStorage` + `createJSONStorage` (Jotai's built-in persistence with error-safe JSON parsing) | +| Memory Leak | Potential memory leak due to uncleaned `handleMouseMove` listener in `usePan`. | `cinny/src/app/hooks/usePan.ts` | FALSE POSITIVE β€” `usePan` already uses `attachedRef` to track listeners and cleans them up in an unmount `useEffect`. No change needed. | +| Asset Optimization | Large unoptimized media asset (213KB) found in `public/res`. | `public/res/Lotus.png` | OPEN | +| Data Persistence | Non-atomic `localStorage` updates in session management can lead to inconsistent state. | `cinny/src/app/state/sessions.ts` | OPEN | +| Data Persistence | Lack of cross-tab synchronization for `localStorage` updates in session management risks race conditions. | `cinny/src/app/state/sessions.ts` | OPEN | +| Network Resilience | `uploadContent` lacks retry logic, failing immediately upon network error. | `cinny/src/app/utils/matrix.ts` | OPEN | +| Network Resilience | `rateLimitedActions` uses basic retry logic without exponential backoff, which may exacerbate 429 issues. | `cinny/src/app/utils/matrix.ts` | FIXED β€” fallback delay now uses capped exponential backoff (`min(1000 * 2^retryCount, 30_000)ms`) when server doesn't send `Retry-After`; server header still takes precedence via `getRetryAfterMs()`. | +| Matrix Event Robustness | `useMatrixEventRenderer` handles unknown events gracefully by returning `null`, which may hide potentially important unrendered data. | `cinny/src/app/hooks/useMatrixEventRenderer.ts` | OPEN | +| Data Contract | `MatrixError` instantiation with `UploadResponse` might be brittle. | `cinny/src/app/utils/matrix.ts` | OPEN | +| Type Safety | `addRoomIdToMDirect` uses `as any` cast for `AccountDataEvent.Direct`, bypassing type contract validation. | `cinny/src/app/utils/matrix.ts` | OPEN | +| Robustness | `rateLimitedActions` relies on `MatrixError.httpStatus` which might not exist on all error variants. | `cinny/src/app/utils/matrix.ts` | FALSE POSITIVE β€” `MatrixError.httpStatus` is defined as `readonly httpStatus?: number` in `matrix-js-sdk/lib/http-api/errors.d.ts`. It is optional (not on all instances) but the `?.` optional chain already guards against undefined. No change needed. | +| Type Contract | Custom types in `cinny/src/types/matrix` mirror SDK types instead of using them, risking drift and contract mismatches. | `cinny/src/types/matrix/` | OPEN | ## πŸ—οΈ Architectural & Hygiene Audit @@ -157,50 +157,50 @@ This document tracks identified bugs, edge cases, and architectural discrepancie ## πŸ—οΈ TDS Compliance & Styling Issues -| Issue Description | File Path | -| :-------------------------------------------------------------------- | :-------------------------------------------------------- | -| Hardcoded inline style `cursor: 'pointer'` | `cinny/src/app/plugins/react-custom-html-parser.tsx` | -| Hardcoded color `#00D4FF`, `#FFB300` βœ… **VERIFIED COMPLIANT** | `cinny/src/app/components/event-readers/EventReaders.tsx` | -| Hardcoded color `#EE1D52`, `#9146ff`, `#ff4500`, `#cb3837`, `#f48024` ⚠️ **BRAND EXCEPTION** | `cinny/src/app/components/url-preview/UrlPreviewCard.tsx` + `UrlPreview.css.tsx` β€” official third-party brand colors in SVG logos and site badge backgrounds; cannot convert to CSS variables without inventing new tokens (violates TDS rule 3) | -| Massive number of hardcoded `backgroundColor` values ⚠️ **PATTERN CONTENT EXCEPTION** | `cinny/src/app/features/lotus/chatBackground.ts` β€” each background's base color is aesthetic content that defines the pattern identity; converting requires inventing 40+ CSS variables (violates TDS rule 3) or using CSS4 `relative-color-syntax` in inline styles (insufficient browser support); these are visual content, not UI chrome | -| Hardcoded colors `#00FF88`, `#FF6B00` βœ… **VERIFIED COMPLIANT** | `cinny/src/app/features/call/CallControls.tsx` | -| Hardcoded fallback hexes in toast colors βœ… **FIXED** | `cinny/src/app/features/toast/LotusToastContainer.tsx` | +| Issue Description | File Path | +| :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hardcoded inline style `cursor: 'pointer'` | `cinny/src/app/plugins/react-custom-html-parser.tsx` | +| Hardcoded color `#00D4FF`, `#FFB300` βœ… **VERIFIED COMPLIANT** | `cinny/src/app/components/event-readers/EventReaders.tsx` | +| Hardcoded color `#EE1D52`, `#9146ff`, `#ff4500`, `#cb3837`, `#f48024` ⚠️ **BRAND EXCEPTION** | `cinny/src/app/components/url-preview/UrlPreviewCard.tsx` + `UrlPreview.css.tsx` β€” official third-party brand colors in SVG logos and site badge backgrounds; cannot convert to CSS variables without inventing new tokens (violates TDS rule 3) | +| Massive number of hardcoded `backgroundColor` values ⚠️ **PATTERN CONTENT EXCEPTION** | `cinny/src/app/features/lotus/chatBackground.ts` β€” each background's base color is aesthetic content that defines the pattern identity; converting requires inventing 40+ CSS variables (violates TDS rule 3) or using CSS4 `relative-color-syntax` in inline styles (insufficient browser support); these are visual content, not UI chrome | +| Hardcoded colors `#00FF88`, `#FF6B00` βœ… **VERIFIED COMPLIANT** | `cinny/src/app/features/call/CallControls.tsx` | +| Hardcoded fallback hexes in toast colors βœ… **FIXED** | `cinny/src/app/features/toast/LotusToastContainer.tsx` | --- ## 🌐 Localization, Accessibility & Performance -| Category | Issue Description | File Path | Status | -| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------- | :----- | -| Localization | Hardcoded UI string: "Chat Room" | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | -| Localization | Hardcoded UI string: "Messages, photos, and videos." | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | -| Localization | Hardcoded UI string: "Voice Room" | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | -| Localization | Hardcoded UI string: "Live audio and video conversations." | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | -| Localization | Hardcoded UI string: "Download" | `src/app/components/image-viewer/ImageViewer.tsx` | OPEN | -| Localization | Hardcoded UI string: "Open Location" | `src/app/components/message/MsgTypeRenderers.tsx` | OPEN | -| Localization | Hardcoded UI string: "Thread" | `src/app/components/message/Reply.tsx` | OPEN | -| Localization | Hardcoded UI string: "View" | `src/app/components/message/content/ImageContent.tsx` | OPEN | -| Localization | Hardcoded UI string: "Spoiler" | `src/app/components/message/content/ImageContent.tsx` | OPEN | -| Localization | Hardcoded UI string: "Retry" | `src/app/components/message/content/ImageContent.tsx` | OPEN | -| Localization | Hardcoded UI string: "Close" | `src/app/components/DeviceVerification.tsx` | OPEN | -| Localization | Hardcoded UI string: "Accept" | `src/app/components/DeviceVerification.tsx` | OPEN | -| Localization | Hardcoded UI string: "They Match" | `src/app/components/DeviceVerification.tsx` | OPEN | -| Localization | Hardcoded UI string: "Okay" | `src/app/components/DeviceVerification.tsx` | OPEN | -| Localization | Hardcoded UI string: "Join Server" | `src/app/components/url-preview/UrlPreviewCard.tsx` | OPEN | -| Localization | Hardcoded UI string: "Invite" | `src/app/components/invite-user-prompt/InviteUserPrompt.tsx` | OPEN | -| Localization | Hardcoded UI string: "Files" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN | -| Localization | Hardcoded UI string: "Send" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN | -| Localization | Hardcoded UI string: "Upload Failed" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN | -| Localization | Hardcoded UI string: "Password" | `src/app/components/uia-stages/PasswordStage.tsx` | OPEN | -| Bundle Size | Large unoptimized media asset (213KB) | `public/res/Lotus.png` | OPEN | -| Matrix Logic | Inefficient repeated `mx.getRoom()` calls in component render loops | `src/app/features/lobby/Lobby.tsx` | OPEN | -| Matrix Logic | Inefficient repeated `mx.getRoom()` calls in component render loops | `src/app/components/emoji-board/EmojiBoard.tsx` | OPEN | -| Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN | -| Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN | -| Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | FIXED ⚠️ UNTESTED β€” added `aria-label="View edit history"` | +| Category | Issue Description | File Path | Status | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Localization | Hardcoded UI string: "Chat Room" | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | +| Localization | Hardcoded UI string: "Messages, photos, and videos." | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | +| Localization | Hardcoded UI string: "Voice Room" | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | +| Localization | Hardcoded UI string: "Live audio and video conversations." | `src/app/components/create-room/CreateRoomTypeSelector.tsx` | OPEN | +| Localization | Hardcoded UI string: "Download" | `src/app/components/image-viewer/ImageViewer.tsx` | OPEN | +| Localization | Hardcoded UI string: "Open Location" | `src/app/components/message/MsgTypeRenderers.tsx` | OPEN | +| Localization | Hardcoded UI string: "Thread" | `src/app/components/message/Reply.tsx` | OPEN | +| Localization | Hardcoded UI string: "View" | `src/app/components/message/content/ImageContent.tsx` | OPEN | +| Localization | Hardcoded UI string: "Spoiler" | `src/app/components/message/content/ImageContent.tsx` | OPEN | +| Localization | Hardcoded UI string: "Retry" | `src/app/components/message/content/ImageContent.tsx` | OPEN | +| Localization | Hardcoded UI string: "Close" | `src/app/components/DeviceVerification.tsx` | OPEN | +| Localization | Hardcoded UI string: "Accept" | `src/app/components/DeviceVerification.tsx` | OPEN | +| Localization | Hardcoded UI string: "They Match" | `src/app/components/DeviceVerification.tsx` | OPEN | +| Localization | Hardcoded UI string: "Okay" | `src/app/components/DeviceVerification.tsx` | OPEN | +| Localization | Hardcoded UI string: "Join Server" | `src/app/components/url-preview/UrlPreviewCard.tsx` | OPEN | +| Localization | Hardcoded UI string: "Invite" | `src/app/components/invite-user-prompt/InviteUserPrompt.tsx` | OPEN | +| Localization | Hardcoded UI string: "Files" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN | +| Localization | Hardcoded UI string: "Send" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN | +| Localization | Hardcoded UI string: "Upload Failed" | `src/app/components/upload-board/UploadBoard.tsx` | OPEN | +| Localization | Hardcoded UI string: "Password" | `src/app/components/uia-stages/PasswordStage.tsx` | OPEN | +| Bundle Size | Large unoptimized media asset (213KB) | `public/res/Lotus.png` | OPEN | +| Matrix Logic | Inefficient repeated `mx.getRoom()` calls in component render loops | `src/app/features/lobby/Lobby.tsx` | OPEN | +| Matrix Logic | Inefficient repeated `mx.getRoom()` calls in component render loops | `src/app/components/emoji-board/EmojiBoard.tsx` | OPEN | +| Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN | +| Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN | +| Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | FIXED ⚠️ UNTESTED β€” added `aria-label="View edit history"` | | Accessibility | `button` for reaction lacks `aria-label` | `cinny/src/app/components/message/Reaction.tsx` | **FIXED ⚠️ UNTESTED** β€” `Reaction` component now computes `aria-label="{shortcode} reaction, N people"` internally using `getShortcodeFor`; custom (mxc://) emoji falls back to "custom emoji reaction". | -| Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED β€” added `aria-label="View thread"` | -| Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED β€” added `aria-label="Jump to original message"` | +| Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED β€” added `aria-label="View thread"` | +| Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED β€” added `aria-label="Jump to original message"` | --- @@ -260,3 +260,230 @@ This document tracks identified bugs, edge cases, and architectural discrepancie | Workflow | Monolithic "Fix all bugs" commits (e.g., `10f6544e`, `aa48c9ef`) make `git bisect` difficult. | Git History | OPEN | | Workflow | Inconsistent commit message prefixes (e.g., `fix`, `feat`, `docs`, `assets`). | Git History | OPEN | | Workflow | Use of `fix` or `feat` for large-scale changes affecting multiple disparate systems (e.g., `938ead79`). | Git History | OPEN | + +--- + +## 🎨 Native UI/UX Consistency β€” Lotus vs. Cinny Baseline + +> Audit of every Lotus-custom UI feature against Cinny's native folds design-system conventions. "Native pattern" means the `folds` component library, vanilla-extract tokens (`color.*`, `config.radii.*`, `config.space.*`), and established Cinny component patterns. 52 findings, organized by severity. + +--- + +### πŸ”΄ Major β€” Broken Styling / Functional Regressions + +#### N1. `ProfileDecoration` Save Button β€” Undefined `--accent-cyan` Variable (border invisible on all non-TDS themes) + +- **File:** `src/app/features/settings/account/ProfileDecoration.tsx`, lines 191–213 +- **Status:** **FIXED** β€” replaced raw ` ); } diff --git a/src/app/components/message/content/AudioContent.tsx b/src/app/components/message/content/AudioContent.tsx index f91315174..4aebd3ef3 100644 --- a/src/app/components/message/content/AudioContent.tsx +++ b/src/app/components/message/content/AudioContent.tsx @@ -182,8 +182,8 @@ export function AudioContent({ {`${playbackSpeed}Γ—`} diff --git a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx index aa5adf931..2029a6cfd 100644 --- a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx +++ b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx @@ -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', }} > diff --git a/src/app/components/room-topic-viewer/RoomTopicViewer.tsx b/src/app/components/room-topic-viewer/RoomTopicViewer.tsx index cd586e2a1..974b1daed 100644 --- a/src/app/components/room-topic-viewer/RoomTopicViewer.tsx +++ b/src/app/components/room-topic-viewer/RoomTopicViewer.tsx @@ -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'; diff --git a/src/app/components/upload-card/UploadCardRenderer.tsx b/src/app/components/upload-card/UploadCardRenderer.tsx index 95de16949..af2820327 100644 --- a/src/app/components/upload-card/UploadCardRenderer.tsx +++ b/src/app/components/upload-card/UploadCardRenderer.tsx @@ -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')) && ( - setMetadata(fileItem, { ...metadata, caption: e.target.value })} + onChange={(e: React.ChangeEvent) => + 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%' }} /> )} diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index bd769b425..830bde89e 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -89,7 +89,7 @@ function UserDeviceRow({ userId, device }: UserDeviceRowProps) { return ( {(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 ( - + {device.displayName ?? device.deviceId} @@ -239,7 +239,7 @@ function UserPrivateNotes({ userId }: { userId: string }) { Private Note - + {saving ? 'Saving…' : charsLeft < 100 ? `${charsLeft} left` : ''} @@ -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', diff --git a/src/app/features/common-settings/general/RoomProfile.tsx b/src/app/features/common-settings/general/RoomProfile.tsx index 4de33bb65..71cd53827 100644 --- a/src/app/features/common-settings/general/RoomProfile.tsx +++ b/src/app/features/common-settings/general/RoomProfile.tsx @@ -4,7 +4,6 @@ import { Button, Chip, color, - config, Icon, IconButton, Icons, @@ -18,6 +17,7 @@ import { import React, { FormEventHandler, useCallback, useMemo, useRef, useState } from 'react'; import { useAtomValue } from 'jotai'; import Linkify from 'linkify-react'; +import parse from 'html-react-parser'; import classNames from 'classnames'; import { JoinRule, MatrixError } from 'matrix-js-sdk'; import { EmojiBoard } from '../../../components/emoji-board'; @@ -33,6 +33,7 @@ import { import { mDirectAtom } from '../../../state/mDirectList'; import { BreakWord, LineClamp3 } from '../../../styles/Text.css'; import { LINKIFY_OPTS } from '../../../plugins/react-custom-html-parser'; +import { sanitizeCustomHtml } from '../../../utils/sanitize'; import { RoomAvatar, RoomIcon } from '../../../components/room-avatar'; import { mxcUrlToHttp } from '../../../utils/matrix'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; @@ -84,7 +85,7 @@ function buildTopicContent(topic: string): Record { const formattedBody = topicMarkdownToHtml(topic); // Use HTML-stripped text as the plain topic so the header shows clean text, not raw markdown syntax const plainTopic = formattedBody.replace(/
/g, '\n').replace(/<[^>]+>/g, ''); - // eslint-disable-next-line @typescript-eslint/naming-convention + return { topic: plainTopic, format: 'org.matrix.custom.html', formatted_body: formattedBody }; } @@ -332,30 +333,30 @@ export function RoomProfileEdit({ { label: '`', syntax: '`', placeholder: 'code', title: 'Inline Code' }, ] as const ).map(({ label, syntax, placeholder, title }) => ( - + + {label} + + ))}
)} @@ -456,7 +457,12 @@ export function RoomProfile({ permissions }: RoomProfileProps) {
{topic && ( - {topic.topic} + {topic.format === 'org.matrix.custom.html' && + typeof topic.formatted_body === 'string' ? ( + parse(sanitizeCustomHtml(topic.formatted_body)) + ) : ( + {topic.topic} + )} )}
diff --git a/src/app/features/message-search/SearchFilters.tsx b/src/app/features/message-search/SearchFilters.tsx index 4735168e3..a8c40276b 100644 --- a/src/app/features/message-search/SearchFilters.tsx +++ b/src/app/features/message-search/SearchFilters.tsx @@ -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 = (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 > - + From - + - + {users.length === 0 && ( - No match found! + + No match found! + )}
{vItems.map((vItem) => { @@ -450,7 +469,9 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro aria-pressed={selected} before={} > - {name} + + {name} + ); @@ -467,7 +488,14 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro Save )} - @@ -477,7 +505,9 @@ function SelectSenderButton({ selectedSenders, onChange }: SelectSenderButtonPro } > ) => setMenuAnchor(e.currentTarget.getBoundingClientRect())} + onClick={(e: React.MouseEvent) => + setMenuAnchor(e.currentTarget.getBoundingClientRect()) + } variant="SurfaceVariant" radii="Pill" before={} @@ -529,22 +559,28 @@ function DateRangeButton({ fromTs, toTs, onChange }: DateRangeButtonProps) { Quick pick - {([ - { 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 ( { onChange(from, now); setMenuAnchor(undefined); }} + onClick={() => { + onChange(from, now); + setMenuAnchor(undefined); + }} > {l} @@ -746,13 +782,11 @@ export function SearchFilters({ ); })} - + } @@ -761,7 +795,10 @@ export function SearchFilters({ { e.stopPropagation(); onContainsUrlChange(undefined); }} + onClick={(e) => { + e.stopPropagation(); + onContainsUrlChange(undefined); + }} /> ) : undefined } diff --git a/src/app/features/room-nav/RoomNavItem.tsx b/src/app/features/room-nav/RoomNavItem.tsx index b4c02d506..a8d5606e0 100644 --- a/src/app/features/room-nav/RoomNavItem.tsx +++ b/src/app/features/room-nav/RoomNavItem.tsx @@ -453,12 +453,12 @@ const RoomNavItemMenu = forwardRef( > } after={} radii="300" aria-pressed={!!muteMenuAnchor} onClick={(e) => setMuteMenuAnchor(e.currentTarget.getBoundingClientRect())} > - Mute diff --git a/src/app/features/room-settings/RoomInsights.tsx b/src/app/features/room-settings/RoomInsights.tsx index 7b616923f..87c005c1b 100644 --- a/src/app/features/room-settings/RoomInsights.tsx +++ b/src/app/features/room-settings/RoomInsights.tsx @@ -2,6 +2,7 @@ import React, { useMemo } from 'react'; import { Avatar, Box, Icon, IconButton, Icons, Scroll, Text, color, config } from 'folds'; import { EventType } from 'matrix-js-sdk'; import { Page, PageContent, PageHeader } from '../../components/page'; +import { SequenceCard } from '../../components/sequence-card'; import { useRoom } from '../../hooks/useRoom'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; @@ -23,14 +24,7 @@ function formatDate(ts: number): string { function SectionHeader({ label }: { label: string }) { return ( - + {label} ); @@ -165,31 +159,22 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { {/* ── Disclaimer banner ── */} - - + + - + Based on {stats.totalMessages} locally cached message {stats.totalMessages !== 1 ? 's' : ''} {stats.oldestTs !== null && stats.newestTs !== null && ( - + from {formatDate(stats.oldestTs)} to {formatDate(stats.newestTs)} )} - + {/* ── Summary row ── */} @@ -350,7 +335,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { height: 6, width: barWidth, background: color.Primary.Main, - borderRadius: 3, + borderRadius: config.radii.R300, transition: 'width 0.3s ease', flexShrink: 0, }} @@ -432,7 +417,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { count > 0 && count === maxHour ? color.Primary.Main : color.SurfaceVariant.Container, - borderRadius: '2px 2px 0 0', + borderRadius: `${config.radii.R300} ${config.radii.R300} 0 0`, transition: 'height 0.2s ease', }} /> @@ -445,7 +430,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { {stats.hourBuckets.map((_, h) => ( {h % 6 === 0 ? ( - + {h} ) : null} diff --git a/src/app/features/room/MediaGallery.tsx b/src/app/features/room/MediaGallery.tsx index 6bb489c36..2fdee001d 100644 --- a/src/app/features/room/MediaGallery.tsx +++ b/src/app/features/room/MediaGallery.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useNearViewport } from '../../hooks/useNearViewport'; import { Box, Button, @@ -16,6 +15,7 @@ import { config, } from 'folds'; import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from 'matrix-js-sdk'; +import { useNearViewport } from '../../hooks/useNearViewport'; import { IEncryptedFile, IImageInfo, IThumbnailContent } from '../../../types/matrix/common'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; @@ -373,7 +373,14 @@ function GalleryTile({ const mx = useMatrixClient(); const tileRef = useRef(null); const nearViewport = useNearViewport(tileRef, 300); - const media = useDecryptedMediaUrl(mx, mxcUrl, encInfo, useAuthentication, mimeType, nearViewport); + const media = useDecryptedMediaUrl( + mx, + mxcUrl, + encInfo, + useAuthentication, + mimeType, + nearViewport, + ); const [hovered, setHovered] = useState(false); const relDate = formatRelativeDate(ts); @@ -422,7 +429,13 @@ function GalleryTile({ {body} )} diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 828cbf0c5..2584ee3f0 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -24,6 +24,7 @@ import { Spinner, Button, } from 'folds'; +import { useAtom } from 'jotai'; import { useNavigate } from 'react-router-dom'; import { Room } from 'matrix-js-sdk'; import { useStateEvent } from '../../hooks/useStateEvent'; @@ -74,7 +75,6 @@ import { useLivekitSupport } from '../../hooks/useLivekitSupport'; import { webRTCSupported } from '../../utils/rtc'; import { MediaGallery } from './MediaGallery'; import { usePendingKnocks } from '../../hooks/usePendingKnocks'; -import { useAtom } from 'jotai'; import { bookmarksPanelAtom } from '../../state/bookmarksPanel'; type RoomMenuProps = { @@ -85,247 +85,247 @@ type RoomMenuProps = { }; const RoomMenu = forwardRef( ({ room, requestClose, galleryOpen, onToggleGallery }, ref) => { - const mx = useMatrixClient(); - const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); - const screenSize = useScreenSizeContext(); - const unread = useRoomUnread(room.roomId, roomToUnreadAtom); - const powerLevels = usePowerLevelsContext(); - const creators = useRoomCreators(room); + const mx = useMatrixClient(); + const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); + const screenSize = useScreenSizeContext(); + const unread = useRoomUnread(room.roomId, roomToUnreadAtom); + const powerLevels = usePowerLevelsContext(); + const creators = useRoomCreators(room); - const permissions = useRoomPermissions(creators, powerLevels); - const canInvite = permissions.action('invite', mx.getSafeUserId()); - const isServerNotice = room.getType() === 'm.server_notice'; - const isCreator = creators.has(mx.getSafeUserId()); - const notificationPreferences = useRoomsNotificationPreferencesContext(); - const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); - const { navigateRoom } = useRoomNavigate(); + const permissions = useRoomPermissions(creators, powerLevels); + const canInvite = permissions.action('invite', mx.getSafeUserId()); + const isServerNotice = room.getType() === 'm.server_notice'; + const isCreator = creators.has(mx.getSafeUserId()); + const notificationPreferences = useRoomsNotificationPreferencesContext(); + const notificationMode = getRoomNotificationMode(notificationPreferences, room.roomId); + const { navigateRoom } = useRoomNavigate(); - const [invitePrompt, setInvitePrompt] = useState(false); - const [reportRoomOpen, setReportRoomOpen] = useState(false); - const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom); - const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); + const [invitePrompt, setInvitePrompt] = useState(false); + const [reportRoomOpen, setReportRoomOpen] = useState(false); + const [bookmarksOpen, setBookmarksOpen] = useAtom(bookmarksPanelAtom); + const [peopleDrawer, setPeopleDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); - const handleMarkAsRead = () => { - markAsRead(mx, room.roomId, hideActivity); - requestClose(); - }; + const handleMarkAsRead = () => { + markAsRead(mx, room.roomId, hideActivity); + requestClose(); + }; - const handleInvite = () => { - setInvitePrompt(true); - }; + const handleInvite = () => { + setInvitePrompt(true); + }; - const openSettings = useOpenRoomSettings(); - const parentSpace = useSpaceOptionally(); - const handleOpenSettings = () => { - openSettings(room.roomId, parentSpace?.roomId); - requestClose(); - }; + const openSettings = useOpenRoomSettings(); + const parentSpace = useSpaceOptionally(); + const handleOpenSettings = () => { + openSettings(room.roomId, parentSpace?.roomId); + requestClose(); + }; - return ( - - {invitePrompt && ( - { - setInvitePrompt(false); - requestClose(); - }} - /> - )} - {reportRoomOpen && ( - { - setReportRoomOpen(false); - requestClose(); - }} - /> - )} - - } - radii="300" - disabled={!unread} - > - - Mark as Read - - - - {(handleOpen, opened, changing) => ( + return ( + + {invitePrompt && ( + { + setInvitePrompt(false); + requestClose(); + }} + /> + )} + {reportRoomOpen && ( + { + setReportRoomOpen(false); + requestClose(); + }} + /> + )} + + } + radii="300" + disabled={!unread} + > + + Mark as Read + + + + {(handleOpen, opened, changing) => ( + + ) : ( + + ) + } + radii="300" + aria-pressed={opened} + onClick={handleOpen} + > + + Notifications + + + )} + + + + + { + setBookmarksOpen((v) => !v); + requestClose(); + }} + size="300" + after={} + radii="300" + aria-pressed={bookmarksOpen} + > + + Saved Messages + + + {screenSize === ScreenSize.Mobile && ( { + setPeopleDrawer(!peopleDrawer); + requestClose(); + }} size="300" - after={ - changing ? ( - - ) : ( - - ) - } + after={} radii="300" - aria-pressed={opened} - onClick={handleOpen} + aria-pressed={peopleDrawer} > - Notifications + Members )} - - - - - { - setBookmarksOpen((v) => !v); - requestClose(); - }} - size="300" - after={} - radii="300" - aria-pressed={bookmarksOpen} - > - - Saved Messages - - - {screenSize === ScreenSize.Mobile && ( - { - setPeopleDrawer(!peopleDrawer); - requestClose(); - }} - size="300" - after={} - radii="300" - aria-pressed={peopleDrawer} - > - - Members - - - )} - {screenSize === ScreenSize.Mobile && onToggleGallery && ( - { - onToggleGallery(); - requestClose(); - }} - size="300" - after={} - radii="300" - aria-pressed={galleryOpen} - > - - Media Gallery - - - )} - {!isServerNotice && ( - } - radii="300" - aria-pressed={invitePrompt} - disabled={!canInvite} - > - - Invite - - - )} - {!isServerNotice && ( - } - radii="300" - > - - Room Settings - - - )} - - {(promptJump, setPromptJump) => ( - <> - setPromptJump(true)} - size="300" - after={} - radii="300" - aria-pressed={promptJump} - > - - Jump to Time - - - {promptJump && ( - { - setPromptJump(false); - navigateRoom(room.roomId, eventId); - requestClose(); - }} - onCancel={() => setPromptJump(false)} - /> - )} - + {screenSize === ScreenSize.Mobile && onToggleGallery && ( + { + onToggleGallery(); + requestClose(); + }} + size="300" + after={} + radii="300" + aria-pressed={galleryOpen} + > + + Media Gallery + + )} - - - - - {!isServerNotice && !isCreator && ( - setReportRoomOpen(true)} - variant="Critical" - fill="None" - size="300" - after={} - radii="300" - aria-pressed={reportRoomOpen} - > - - Report Room - - - )} - - {(promptLeave, setPromptLeave) => ( - <> - setPromptLeave(true)} - variant="Critical" - fill="None" - size="300" - after={} - radii="300" - aria-pressed={promptLeave} - > - - Leave Room - - - {promptLeave && ( - setPromptLeave(false)} - /> - )} - + {!isServerNotice && ( + } + radii="300" + aria-pressed={invitePrompt} + disabled={!canInvite} + > + + Invite + + )} - - - - ); -}, + {!isServerNotice && ( + } + radii="300" + > + + Room Settings + + + )} + + {(promptJump, setPromptJump) => ( + <> + setPromptJump(true)} + size="300" + after={} + radii="300" + aria-pressed={promptJump} + > + + Jump to Time + + + {promptJump && ( + { + setPromptJump(false); + navigateRoom(room.roomId, eventId); + requestClose(); + }} + onCancel={() => setPromptJump(false)} + /> + )} + + )} + + + + + {!isServerNotice && !isCreator && ( + setReportRoomOpen(true)} + variant="Critical" + fill="None" + size="300" + after={} + radii="300" + aria-pressed={reportRoomOpen} + > + + Report Room + + + )} + + {(promptLeave, setPromptLeave) => ( + <> + setPromptLeave(true)} + variant="Critical" + fill="None" + size="300" + after={} + radii="300" + aria-pressed={promptLeave} + > + + Leave Room + + + {promptLeave && ( + setPromptLeave(false)} + /> + )} + + )} + + + + ); + }, ); type CallMenuProps = { @@ -567,7 +567,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) { } > {(triggerRef) => ( - + Server Notice )} diff --git a/src/app/features/room/message/Message.tsx b/src/app/features/room/message/Message.tsx index 7048ef87a..5f6b0a055 100644 --- a/src/app/features/room/message/Message.tsx +++ b/src/app/features/room/message/Message.tsx @@ -7,6 +7,7 @@ import { Header, Icon, IconButton, + IconSrc, Icons, Input, Line, @@ -95,23 +96,20 @@ function DeliveryStatus({ lotusTerminal: boolean; }) { if (status === null) return null; // confirmed by server β€” read receipts take over - let icon: string; + let iconSrc: IconSrc; let label: string; let colorStyle: string; + const isSending = status === EventStatus.SENDING || status === EventStatus.ENCRYPTING; if (status === EventStatus.NOT_SENT || status === EventStatus.CANCELLED) { - icon = 'βœ•'; + iconSrc = Icons.Cross; label = 'Failed to send'; colorStyle = lotusTerminal ? '#FF3B3B' : color.Critical.Main; - } else if (status === EventStatus.QUEUED) { - icon = '⟳'; - label = 'Queued'; - colorStyle = lotusTerminal ? 'rgba(0,212,255,0.45)' : color.Secondary.Main; - } else if (status === EventStatus.SENDING || status === EventStatus.ENCRYPTING) { - icon = '⟳'; - label = 'Sending...'; + } else if (status === EventStatus.QUEUED || isSending) { + iconSrc = Icons.Send; + label = isSending ? 'Sending...' : 'Queued'; colorStyle = lotusTerminal ? 'rgba(0,212,255,0.60)' : color.Secondary.Main; } else { - icon = 'βœ“'; + iconSrc = Icons.Check; label = 'Sent'; colorStyle = lotusTerminal ? 'rgba(0,212,255,0.70)' : color.Secondary.Main; } @@ -124,7 +122,6 @@ function DeliveryStatus({ display: 'inline-flex', alignItems: 'center', marginTop: '2px', - fontSize: '10px', lineHeight: 1, color: colorStyle, opacity: 0.85, @@ -134,14 +131,8 @@ function DeliveryStatus({ : {}), }} > - - {icon} + + ); @@ -157,7 +148,7 @@ export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>( const mx = useMatrixClient(); const recentEmojis = useRecentEmoji(mx, 3); - if (recentEmojis.length === 0) return ; + if (recentEmojis.length === 0) return null; return ( <> } + after={} radii="300" onClick={() => { setForwardOpen(true); diff --git a/src/app/features/settings/account/ProfileDecoration.tsx b/src/app/features/settings/account/ProfileDecoration.tsx index f5203c734..9c8f470b5 100644 --- a/src/app/features/settings/account/ProfileDecoration.tsx +++ b/src/app/features/settings/account/ProfileDecoration.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { Box, Text, Spinner } from 'folds'; +import { Box, Button, Text, Spinner, color } from 'folds'; import { Method } from 'matrix-js-sdk'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; @@ -170,51 +170,36 @@ export function ProfileDecoration() { : 'None'} {selected && ( - + Remove + )} {hasChanges && ( - + {saving ? 'Saving…' : 'Save'} + )} {saveState.status === AsyncStatus.Error && ( - + Failed to save. Try again. )} diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index e07cf3386..ba8b4f619 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -1658,7 +1658,7 @@ function SeasonalBgGrid({ borderRadius: toRem(8), cursor: 'pointer', border: selected - ? `2px solid ${color.Critical.Main}` + ? `2px solid ${color.Primary.Main}` : '2px solid rgba(128,128,128,0.25)', padding: 0, overflow: 'hidden', @@ -1687,7 +1687,7 @@ function SeasonalBgGrid({ )}
- + {opt.label}
@@ -1724,7 +1724,7 @@ function ChatBgGrid() { cursor: 'pointer', border: chatBackground === opt.value - ? `2px solid ${color.Critical.Main}` + ? `2px solid ${color.Primary.Main}` : '2px solid rgba(128,128,128,0.25)', padding: 0, overflow: 'hidden', @@ -1733,7 +1733,7 @@ function ChatBgGrid() { /> {opt.label} diff --git a/src/app/features/toast/LotusToastContainer.tsx b/src/app/features/toast/LotusToastContainer.tsx index 09f6b511f..c97a2568c 100644 --- a/src/app/features/toast/LotusToastContainer.tsx +++ b/src/app/features/toast/LotusToastContainer.tsx @@ -194,7 +194,7 @@ export function LotusToastContainer() { position: 'fixed', bottom: '1.5rem', right: '1.5rem', - zIndex: 9997, + zIndex: 10001, display: 'flex', flexDirection: 'column', gap: '8px', diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index 0193f40d4..decae09a2 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -32,13 +32,18 @@ function AppearanceEffects() { const color = settings.mentionHighlightColor; if (color) { document.body.style.setProperty('--mention-highlight-bg', color); - // compute black or white text based on hex luminance + // WCAG 2.1 relative luminance with gamma linearization + const toLinear = (c: number) => { + const s = c / 255; + return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4; + }; const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); - const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255; - document.body.style.setProperty('--mention-highlight-text', lum > 0.5 ? '#000' : '#fff'); - document.body.style.setProperty('--mention-highlight-border', color); + const lum = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); + document.body.style.setProperty('--mention-highlight-text', lum > 0.179 ? '#000' : '#fff'); + // Derive a visible border: same hue, reduced alpha + document.body.style.setProperty('--mention-highlight-border', `rgba(${r},${g},${b},0.5)`); } else { document.body.style.removeProperty('--mention-highlight-bg'); document.body.style.removeProperty('--mention-highlight-text');