Fix settings modal regression: Modal500 was wrapped in useModalStyle(560), forcing maxWidth 560px and squishing the two-pane Settings layout (folds size="500" is ~50rem). Restore desktop width to the folds recipe while keeping mobile fullscreen. N-series fixes: - N13 ScheduledMessagesTray header: <Box as="button"> -> folds <Button> - N28 composer char counter: drop undefined --tc-surface-low + opacity, use priority="300" and config.space token - N31 collapsible "Read more" toggle: padded <Button> -> flush inline-button pattern matching (edited) link - N41 UserPrivateNotes "Saving..." now shows a folds <Spinner> - N43 Night Light slider: add accentColor; label opacity -> priority - N44 mention-highlight Reset: bare <button> -> folds <Button> (drops undefined --border-interactive-normal); Boot button kept (TDS-only) - N45 SelectTheme trigger variant -> Secondary to match SettingsSelect - N49 RoomInsights StatTile emoji -> folds <Icon> (Photo/VideoCamera/ Headphone/File) - N54/N57 PiP overlay badges + fullscreen button: token discipline (config.radii/space, folds Text); dark scrim kept for video legibility - N60 knock badge: match Pinned Messages pattern (no wrapper div, toRem insets, no hardcoded size overrides) - N62 unverified-device banner: 3px left-accent -> standard border via color.Warning.ContainerLine; drop opacity hacks - N65 Edit History: real "Load more" pagination (accumulate next_batch, de-dupe by id, re-sort by ts) replacing passive text - N66 search date fields: raw <input type="date"> -> folds <Input> - N67 SeasonalEffect z-index 9999 -> 9997 (below Night Light + modals) - N73 Pending Requests header uses css.MembersGroupLabel - N74 remove raw em-sized emoji <span> in RoomNavItem name - N85/N86 RemindMeDialog: <Box role="dialog"> -> folds <Dialog>; preset MenuItems -> Buttons (fixes invalid menuitem-in-dialog ARIA) Document deliberate WON'T FIX rationale for N9, N51, N61, N71, N75, N77. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 KiB
Lotus Chat — Bug Report & Technical Audit
Date: June 2026
This document tracks identified bugs, edge cases, and architectural discrepancies found during the audit of the Lotus Chat codebase. Recommended fixes are provided for each item.
🚩 Critical & UI Bugs
12. PiP Mute Icon Misidentifies Whose Mic Is Muted
- File:
cinny/src/app/components/CallEmbedProvider.tsx - Status: FIXED ⚠️ UNTESTED — needs verification in a live call with at least one other participant who mutes/unmutes
- Issue: The muted-mic badge in the Picture-in-Picture window used
useRemoteAllMuted(fires when ANY remote participant is muted) and rendered in the bottom-left corner — the conventional position for "YOUR" mic status. Users read it as their own mic being muted. - Root Cause:
PipMuteOverlaywas triggering on remote-mute events while displaying in a position that implies local-user status. - Fix Applied:
- Bottom-left badge now shows only when the LOCAL user's mic is muted (checked via
!controlState.microphonefromuseCallControlState). Includes "You" label to make it unambiguous. Usescolor.Critical.Main. - Top-right badge (new) shows "All muted" in
color.Warning.Mainwhen all remote participants are muted — positioned and labeled so it's clearly about other people, not the local user. - Both badges use
aria-label/titlefor accessibility.
- Bottom-left badge now shows only when the LOCAL user's mic is muted (checked via
1. No Camera Focus During Screenshare
- File:
cinny/src/app/features/call/CallControls.tsx - Status: OPEN
- Issue: Automatic screenshare spotlighting forces primary display override, preventing users from manually focusing on camera feeds.
- Root Cause: Current spotlighting logic prioritizes active screenshare streams over manual participant selections, effectively ignoring or overriding user-initiated focus states.
- Proposed Fix: Introduce a manual 'Focus' state that takes precedence over automatic screenshare spotlighting, implemented via a toggle/click UI on participant tiles. Update the video renderer to respect this manual override.
2. Chat Background Animation Flickering
- File:
cinny/src/app/features/lotus/chatBackground.ts - Status: FIXED ⚠️ UNTESTED — needs verification on a real device with an animated background active
- Issue: Animated background properties cause visible flickering on message text and the composer area, particularly on browsers/GPUs susceptible to repaint-induced artifacts.
- Root Cause: Animation triggers excessive repaints or layout recalculations on descendant elements, likely due to animating non-GPU accelerated properties on parent containers without proper rendering context isolation.
- Fix Applied:
getChatBg()now injectswillChange: 'background-position'andcontain: 'paint'for any animated variant. This promotes the element to its own compositor layer and isolates repaints from descendants. Background-position animation is already GPU-hinted on modern browsers;contain: paintprevents descendant elements from being invalidated during each frame.
3. Avatar Decorations in Element Call
- File:
cinny/src/app/components/avatar-decoration/AvatarDecoration.tsx - Status: OPEN
- Issue: Avatar decorations are failing to render within the call/room interface member lists.
- Root Cause: Likely a mismatch between the expected
memberobject structure required by theAvatarDecorationcomponent and the data actually provided by the call/room UI components. Matrix event data for decorations might not be propagating correctly to these UI member objects. - Proposed Fix: Analyze the data propagation chain from Matrix events to the member object in
cinny/src/app/components/callandroom, ensuring that decoration-related properties are correctly mapped and passed to theAvatarDecorationcomponent.
4. DM and Group Message Calls
- File:
cinny/src/app/components/CallEmbedProvider.tsx - Status: PARTIALLY FIXED ⚠️ UNTESTED — Volume control added. Remaining: ringtone selection, suppression during active calls.
- Issue: Incoming call ringtone is hardcoded, lacks volume control, and is suppressed if the user is already in an active call.
- Root Cause: Ringing logic is tightly coupled to
RTCNotificationevents inCallEmbedProvider.tsx, using a hardcoded audio file path. It lacks an abstraction for sound management or user-configurable settings for ringtones/volumes. - Fix Applied: Added
ringtoneVolumesetting (0–100, default 70).IncomingCallreads this setting and appliesaudioElement.volume = ringtoneVolume / 100beforeplay(). Slider added to Settings → General → Calls section. - Remaining: (a) Ringtone selection (still hardcoded to
call.ogg); (b) Suppression during active calls — not investigated.
5. Seasonal Themes and Chat Backgrounds Design
- File:
cinny/src/app/hooks/useTheme.ts,cinny/src/app/features/lotus/chatBackground.ts - Status: OPEN
- Issue: Basic CSS or random moving lines are insufficient for high-fidelity wallpaper/theming. They lack professional design theory, coherence, and aesthetic depth.
- Root Cause: Current implementation relies on basic CSS, lacks advanced design theory, and does not leverage modern, performant CSS wallpaper techniques.
- Proposed Fix (Extreme Depth Redesign):
- Research-Backed Implementation: Implement advanced design techniques (layered
oklchgradients,backdrop-filterfor refractive "liquid glass" effects, GPU-acceleratedtransformanimations) to create living, breathing backgrounds. - Performance Optimization: Ensure all animations strictly use compositor-thread properties (
transform,opacity) and applycontain: paint/will-change: transformto prevent layout thrashing/flickering. - Design Resources (Examples/Inspiration):
- Uiverse.io Patterns
- MagicPattern CSS Backgrounds
- Prismic Blog: CSS Background Effects
- CSS-Pattern.com (Pure CSS pattern library)
- BGJar (Performance-focused generators)
- Goal: Treat each theme/background as a week-long development sprint to ensure professional polish, WCAG AA contrast compliance for overlaying UI, and seamless integration with the Lotus TDS.
- Research-Backed Implementation: Implement advanced design techniques (layered
6. Exclusive Background vs. Seasonal Choice
- File:
cinny/src/app/state/settings.ts - Status: FIXED ⚠️ UNTESTED — needs verification: (a) pick a background, confirm seasonal theme auto-clears; (b) pick a seasonal theme, confirm background auto-clears; (c) set both via old localStorage data and reload, confirm SeasonalEffect guard suppresses the overlay
- Issue: Concurrent application of both Chat Backgrounds and Seasonal Themes causes visual clutter and high GPU usage.
- Root Cause: These are currently handled as independent settings in the
settingsAtomand applied simultaneously without mutual exclusion. - Fix Applied: Mutual exclusion enforced at two layers: (1)
General.tsx— ChatBgGrid clears seasonalThemeOverride→'off' when any non-'none' background is picked; SeasonalBgGrid clears chatBackground→'none' when any real seasonal theme is selected. (2)SeasonalEffect.tsx— runtime guard returns null ifchatBackground !== 'none', protecting against legacy persisted state.
7. Tiny Touch Targets in Composer Toolbar
- File:
cinny/src/app/features/room/RoomInput.tsx - Status: FIXED ⚠️ UNTESTED — needs verification on a real mobile device: open composer, confirm all toolbar buttons are tappable without mis-taps
- Issue: Toolbar buttons have hit areas smaller than the WCAG-recommended 44x44px for touch, hindering mobile accessibility.
- Fix Applied: Added
touchTarget = { minWidth: '44px', minHeight: '44px' }computed frommobileOrTablet()and applied asstyle={touchTarget}to all 8 composer toolbarIconButtonelements (attach, format, sticker, emoji, GIF, location, poll, schedule, send).
8. Horizontal Overflow in Room Settings
- File:
cinny/src/app/components/page/style.css.ts - Status: FIXED ⚠️ UNTESTED — needs verification: open Room Settings on a narrow mobile screen, confirm nav panel fills full width and no horizontal scrollbar appears
- Issue: Wide tables and input elements in room settings cause horizontal overflow on mobile viewports.
- Fix Applied: Added
@media (max-width: 750px) { width: '100%' }to both'400'and'300'size variants of thePageNavvanilla-extract recipe instyle.css.ts.
9. Modal Float-Style Responsiveness
- File: Multiple modal files
- Status: FIXED ⚠️ UNTESTED — needs verification by opening each modal on a real mobile device
- Issue: Modals appear as floating boxes on mobile, creating navigation and readability challenges.
- Fix Applied: Created
useModalStyle(desktopMaxWidth)hook (src/app/hooks/useModalStyle.ts) that returns fullscreen styles on mobile (no border-radius, no max-width,height: 100%) and desktop box styles otherwise. Applied to all 22+ modal files:LeaveRoomPrompt,LeaveSpacePrompt,ReportRoomModal,ReportUserModal,DeviceVerification,InviteUserPrompt,LogoutDialog,DeviceVerificationSetup,DeviceVerificationReset,JoinAddressPrompt,JumpToTime,EditHistoryModal,ForwardMessageDialog,RemindMeDialog,CreateRoomModal,CreateSpaceModal,ScheduleMessageModal,PollCreator,AddExistingModal,RoomEncryption,RoomUpgrade,Modal500,ReadReceiptAvatars,RoomTopicViewer. - Note:
UIAFlowOverlayalready fullscreen via<Overlay>— no change needed.JoinRulesSwitcher/RoomNotificationSwitcherare dropdowns, not modals.
10. Composer Keyboard Obscurity
- File:
src/index.css - Status: FIXED ⚠️ UNTESTED — needs verification on iOS Safari specifically (the worst offender); on Android Chrome
100dvhhas been standard since Chrome 108 - Issue: The chat composer is often partially or fully obscured by the virtual keyboard on mobile.
- Fix Applied: Added
height: 100dvh(dynamic viewport height) tohtmlalongside the existingheight: 100%fallback.dvhupdates when the software keyboard appears, ensuring the layout shrinks correctly and the composer stays visible.
11. Inline Jotai atom creation
- File:
cinny/src/app/hooks/useSpaceHierarchy.ts - Status: FALSE POSITIVE — CLOSED
- Issue: Inline Jotai atom creation in a hook risks re-rendering components unnecessarily.
- Resolution:
useState(() => atom(...))IS the correct Jotai pattern for local stable atom references. The factory function form ofuseStateensures the atom is created only once per component mount. No change warranted.
📦 Barrel File Audit
| File Path | Note | Status |
|---|---|---|
cinny/src/app/plugins/call/index.ts |
Extensive export * usage |
OPEN |
cinny/src/app/plugins/text-area/index.ts |
Extensive export * usage |
OPEN |
cinny/src/app/components/message/index.ts |
Extensive export * usage |
OPEN |
🔍 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 |
| 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 |
🏗️ Architectural & Hygiene Audit
| Category | Issue Description | File Path | Status |
|---|---|---|---|
| Hygiene | No stale development notes or TypeScript strictness issues found | N/A | OPEN |
🏗️ 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 |
🌐 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" |
| 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" |
🔧 Infrastructure, DevEx & Type Safety
| Category | Issue Description | File Path | Status |
|---|---|---|---|
| Dependencies | lodash pinned to non-existent version 4.18.1 |
cinny/package.json |
OPEN |
| Dependencies | Various pinned versions of @atlaskit, matrix-js-sdk |
cinny/package.json |
OPEN |
| Dependencies | matrix-js-sdk pinned to Release Candidate (41.6.0-rc.0) |
cinny/package.json |
OPEN |
| Dependencies | Unstable/experimental versions for build tools (vite 8.0.14, typescript 6.0.3, eslint 9.39.4) |
cinny/package.json |
OPEN |
| CI/CD | package-manager-cache set to false |
cinny/.github/workflows/build-pull-request.yml |
OPEN |
| CI/CD | Inefficient sequential execution in deployment | cinny/.github/workflows/prod-deploy.yml |
OPEN |
| CI/CD | Aggressive 1-minute timeout for Netlify deploy | cinny/.github/workflows/prod-deploy.yml |
OPEN |
| DevEx | Stale upstream bug tracker link/donations/CLA | cinny/CONTRIBUTING.md |
OPEN |
| DevEx | Alignment issue between README and CONTRIBUTING | cinny/README.md |
OPEN |
| Testing | No evident automated testing configuration/files | cinny/src/ |
OPEN |
| Type Safety | Extensive use of as any type assertions |
cinny/src/ |
OPEN |
| Security | Hardcoded public CDN URL; consider moving to environment variable | /root/code/cinny/scripts/syncDecorations.mjs | OPEN |
| Architecture | Modifying node_modules directly is brittle; use patch-package instead | /root/code/cinny/scripts/patch-folds.mjs | OPEN |
| Robustness | Missing security headers (HSTS, CSP, etc.) and inefficient asset serving using rewrites instead of try_files | /root/code/cinny/contrib/nginx/cinny.domain.tld.conf | OPEN |
| Robustness | Incomplete documentation/placeholder path in Caddyfile | /root/code/cinny/contrib/caddy/caddyfile | OPEN |
| Matrix SDK | Inefficient listener management (setMaxListeners: 150) and incomplete SDK state transition handling. |
src/client/initMatrix.ts |
OPEN |
| PWA Robustness | Service worker lacks caching strategy for application assets, resulting in no offline capability. | cinny/src/sw.ts |
OPEN |
| PWA Integrity | manifest: false in vite.config.js might prevent correct PWA installation if not handled externally. |
cinny/vite.config.js |
OPEN |
| PII Leakage | Potential PII exposure via console.error (parameter e likely contains event data). | cinny/src/app/plugins/call/CallEmbed.ts |
OPEN |
| PII Leakage | Potential PII exposure via console.warn (parameter imgError/videoError/thumbError object). | cinny/src/app/features/room/msgContent.ts |
OPEN |
| PII Leakage | Potential PII exposure via console.error (parameter e likely contains event data). | cinny/src/app/features/room/RoomInput.tsx |
OPEN |
🏗️ Architectural & Resilience Audit
| Category | Issue Description | File Path | Status |
|---|---|---|---|
| Element Call Integration | Lacks robust iframe failure monitoring beyond initial 'preparing' event; can result in a permanently hung 'Loading...' state with no user-visible error or recovery path. | src/app/plugins/call/CallEmbed.ts |
OPEN |
| Component Resilience | RoomTimeline has no ErrorBoundary wrapper — a single malformed event crashing the renderer takes down the entire timeline with no fallback UI. |
src/app/features/room/RoomTimeline.tsx |
OPEN |
| Component Resilience | RoomInput has no ErrorBoundary wrapper — a crash in the composer leaves users unable to send messages. |
src/app/features/room/RoomInput.tsx |
OPEN |
| Fallback Logic | No explicit empty/error fallback for Matrix SDK data calls in RoomTimeline; relies purely on SDK internal error propagation, meaning silent failures show a blank timeline. |
src/app/features/room/RoomTimeline.tsx |
OPEN |
| Dependency | Potential for complex dependency chains due to deep nesting in src/app/features/ and src/app/hooks/. |
src/app/ |
OPEN |
| Hydration/Race Condition | The SyncState listener registered by useSyncState may miss the initial 'PREPARED' event if the client initializes synchronously from IndexedDB before the effect runs, leading to an infinite loading state. | cinny/src/app/pages/client/ClientRoot.tsx |
OPEN |
| Structure | High number of small, highly coupled utility hooks (src/app/hooks/) may obscure dependency graphs. |
src/app/hooks/ |
OPEN |
| Dead Code | Potential for unused CSS modules or UI components in src/app/features/. |
src/app/ |
OPEN |
| Security | Sensitive session data (access tokens, device ID) stored in localStorage is vulnerable to XSS. |
src/app/state/sessions.ts |
OPEN |
| Privacy | Sensitive user status messages and expiry timestamps are persisted in localStorage. |
src/app/features/settings/account/Profile.tsx |
OPEN |
| Privacy | Unsent composer drafts stored in localStorage without encryption could leak info on shared devices. |
src/app/features/room/RoomInput.tsx |
OPEN |
| Persistence | Scheduled messages relying on fragile localStorage parsing are prone to data loss on session expiry or error. |
src/app/state/scheduledMessages.ts |
OPEN |
| Bundle Bloat | Inefficient lodash import; risks including entire library instead of necessary utilities. |
cinny/package.json |
OPEN |
| Bundle Bloat | Large matrix-js-sdk (RC version) dependency; high potential for tree-shaking overhead. |
cinny/package.json |
OPEN |
| Build-Time Overhead | lotusDenoise plugin performs heavy, sequential fs operations during closeBundle, significantly slowing build times. |
cinny/vite.config.js |
OPEN |
| Build-Time Overhead | Complex manual viteStaticCopy configuration requiring multiple renames and path manipulations; risks redundant processing. |
cinny/vite.config.js |
OPEN |
| Architectural Debt | Redundant style variant logic in SpacingVariant could be simplified. |
cinny/src/app/components/message/layout/layout.css.ts |
OPEN |
| Overhead Analysis | Potential CSS bloat from DropTarget composition across multiple recipes (SidebarItem, SidebarFolder). |
cinny/src/app/components/sidebar/Sidebar.css.ts |
OPEN |
🏗️ Git Workflow & History Audit
| Category | Issue Description | File Path | Status |
|---|---|---|---|
| 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
foldscomponent 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
<button>with<Button size="400" variant="Success" fill="Solid" radii="300">, removed undefined--accent-cyanreference - Issue: The save button is a raw
<button>withborder: '1px solid var(--accent-cyan)'andcolor: 'var(--accent-cyan)'. The variable--accent-cyan(without the--lt-prefix) is never defined in any theme file — the correct prefixed form is--lt-accent-cyan. On all non-TDS themes the border is invisible and the text has no color. - Root Cause: Missing
--lt-prefix. Additionally, the raw<button>should be a folds<Button>to match every other save button in the sameProfile.tsxsettings panel (e.g.,ProfileDisplayNamesave atProfile.tsx:303). - Fix: Replace raw
<button>with<Button size="400" variant="Success" fill="Solid" radii="300">. Remove the--accent-cyanreference.
N2. UserPrivateNotes Textarea — Undefined --border-interactive Variable (border invisible on all themes)
- File:
src/app/components/user-profile/UserRoomProfile.tsx, lines 246–265 - Status: FIXED — replaced undefined CSS vars with
color.SurfaceVariant.ContainerLine,config.radii.R300,config.space.S200/S300 - Issue: The notes textarea sets
border: '1px solid var(--border-interactive)'. This variable is never defined anywhere in the codebase — the correct equivalents are--bg-surface-border(src/index.css) orcolor.SurfaceVariant.ContainerLine(folds token). The border is invisible on all themes. - Root Cause: Invented CSS variable name. Also uses raw pixel sizing (
borderRadius: '6px',padding: '8px 10px',fontSize: '14px') instead of folds tokens. - Fix: Replace inline style with
border: \1px solid ${color.SurfaceVariant.ContainerLine}`,borderRadius: config.radii.R300,padding: config.space.S200`.
N3. LotusToastContainer — Z-Index Places Toasts Below Night Light Overlay and All Modals
- File:
src/app/features/toast/LotusToastContainer.tsx, lines 184–211;src/app/pages/App.tsx - Status: FIXED — raised toast
zIndexfrom9997to10001(above Night Light at 9998 and modals at 9999) - Issue: The toast container uses hardcoded
zIndex: 9997. The Night Light overlay is atz-index: 9998. The foldsOverlay/Dialogcomponents used for all modals resolve toz-index: 9999. Result: (a) toasts render under the Night Light tint and take on the warm orange filter; (b) any open modal covers toasts entirely, making notifications invisible. - Root Cause: The toast container does not use the
foldsOverlayContainerProviderportal that manages z-index correctly — it is a plainposition: fixeddiv injected directly inApp.tsx. - Fix: Either route the toast portal through
OverlayContainerProvider(matching how all other floating UI works), or raisezIndexabove all overlay layers (10001+). Also audit Night Light's z-index (9998) relative to toasts.
N4. PollContent Vote Buttons — Entirely Outside the Folds Design System
- File:
src/app/components/message/content/PollContent.tsx, lines 250–358 - Status: OPEN
- Issue: Each poll answer is a native
<button>with ~15 hardcoded inline style properties using undefined CSS variables (--accent-cyan,--accent-cyan-dim,--accent-cyan-border). Checkbox/radio indicators, percentage spans, and the poll label use raw pixel font sizes (0.68rem,0.78rem,0.88rem) and hardcodedborderRadius: '8px'. None of these variables exist in any theme — the entire component will render unstyled on non-TDS themes. All other interactive message content (audio, file, image) uses foldsChiporButtonvariants. - Root Cause: Custom implementation that bypasses folds primitives entirely.
- Fix: Rewrite using folds
ButtonorChipfor answers; replace--accent-cyan*withcolor.Secondary.*folds tokens; useText size="T300"for labels.
🟠 Moderate — Interaction Pattern or Visual Deviations
| # | Area | File | Lines | Issue | Native Pattern |
|---|---|---|---|---|---|
| N5 | Read Receipts | ReadReceiptAvatars.tsx |
62–137 | Trigger button is raw <button> with onMouseEnter/onMouseLeave JS style mutation for hover state |
All interactive elements use useHover from react-aria and folds variant system for hover; direct .style mutation used nowhere else on buttons |
| N6 | Read Receipts | ReadReceiptAvatars.tsx & Message.tsx |
32–56 / 268–283 | Two code paths open EventReaders: avatar-pill path uses useModalStyle(360) for mobile fullscreen; context-menu path (MessageReadReceiptItem) does not — on mobile the context menu path opens a fixed-size non-fullscreen modal for the same content |
All modals that share a layout variant use useModalStyle consistently; MessageReadReceiptItem was not updated when useModalStyle was added |
| N7 | Delivery Status | Message.tsx |
89–148 | DeliveryStatus renders Unicode glyphs (⟳ ✓ ✕) in a <span> with fontSize: '10px' instead of folds <Icon> components — FIXED: replaced with Icons.Check/Cross/Send via <Icon size="100"> |
Icons.Check, Icons.Cross, etc. are used for all other status glyphs; folds Text size tokens for all supplementary text |
| N8 | GIF Picker | GifPicker.tsx |
83–124 | GIF picker container uses fully bespoke inline styles (borderRadius: '12px', boxShadow: '0 8px 32px rgba(0,0,0,0.4)', raw rgba border) — two separate style sets for TDS and non-TDS paths |
EmojiBoard has no caller-applied container styling; folds components handle their own surface internally via design tokens |
| N9 | GIF Button | RoomInput.tsx |
1076–1087 | GIF toolbar button renders <Text size="T200"> with hand-rolled fontWeight/fontSize/letterSpacing instead of <Icon> — WON'T FIX (deliberate): folds has no GIF icon, and "GIF" is a widely-recognized text affordance (Slack/Discord/Element all use a text label). Converting to an arbitrary icon would be less clear, not more. |
All 8 other toolbar buttons (Smile, Sticker, Location, Poll, etc.) use <Icon src={...} /> exclusively |
| N10 | Send Animation | Message.tsx + Animations.css.ts |
979–998 / 60–71 | MsgAppearClass and MentionHighlightPulse both animate transform: scale on the same MessageBase DOM node — on self-sent mention messages both classes apply simultaneously and fight over the transform property |
Pre-existing highlightAnime only animates backgroundColor; no prior transform animation on MessageBase |
| N11 | AvatarDecoration | AvatarDecoration.tsx |
5 / 38–41 | Fixed 8px inset on all sides regardless of avatar size — at folds size "200" (~32px) the decoration bleeds 50% of the avatar diameter, clipping against overflow: hidden parent containers in member lists |
Folds Avatar and PresenceRingAvatar do not emit overflow outside their bounding box |
| N12 | MediaGallery Drawer | MediaGallery.tsx |
651–661 | Drawer uses position: 'fixed' with hardcoded width: '320px' as inline styles on a <Box> |
MembersDrawer uses a vanilla-extract class with width: toRem(266) and is placed by the layout system, not position: fixed. 54px width discrepancy also breaks visual rhythm if both panels could be open |
| N13 | ScheduledMessagesTray | ScheduledMessagesTray.tsx |
108–126 | Collapsible tray header is <Box as="button"> with cursor: 'pointer' inline style and no folds variant — no hover state, no focus ring — FIXED: replaced with folds <Button variant="Secondary" fill="None" radii="0"> using before/after icon props (gains design-system hover/focus) |
All clickable header/toggle elements in the room view use folds <Button> or <IconButton> with explicit variants for hover/focus; <Box as="button"> with no variant is used nowhere else |
| N14 | ForwardMessageDialog | ForwardMessageDialog.tsx |
137–154 | Dialog uses <Modal> but has no <Header> component and no close <IconButton> — only way to close is clicking outside |
Every other modal using <Modal> or <Box role="dialog"> includes a <Header> with a close <IconButton> in the top-right (EditHistoryModal, LeaveRoomPrompt, ScheduleMessageModal, RemindMeDialog, etc.) |
| N15 | ScheduleMessageModal | ScheduleMessageModal.tsx |
180–193 | Modal root is <Box as="form" role="dialog"> with manually assembled borderRadius: config.radii.R400/boxShadow |
ForwardMessageDialog uses folds <Modal size="400"> with R500 radius; the R400 vs R500 mismatch is visible when both dialogs appear in the same session |
| N16 | Presence Picker | SettingsTab.tsx |
118–144 | Presence trigger dot is raw <button> with position: absolute; bottom: 2; right: 2 inline and no folds focus ring; no tooltip |
Every other sidebar icon button uses folds IconButton with SidebarItemTooltip and TooltipProvider |
| N17 | Presence Picker | SettingsTab.tsx |
80–86 | PresencePicker FocusTrap missing escapeDeactivates: stopPropagation and isKeyForward/isKeyBackward |
Every other PopOut+FocusTrap+Menu combo supplies both (theme selector General.tsx:143–160, SettingsSelect, sort menus) — without it Escape bubbles past the trap and arrow-key navigation is absent |
| N18 | Profile Selects | Profile.tsx |
547–575 / 816–848 | ProfileStatus auto-clear and ProfileTimezone selectors are native <select> elements with hardcoded colorScheme: 'dark' — will render in dark mode on light themes |
General.tsx uses folds SettingsSelect<T> (Button+PopOut+Menu) for all dropdowns; colorScheme: 'dark' breaks light/custom theme appearance |
| N19 | Presence Labels | useUserPresence.ts vs SettingsTab.tsx |
55–62 / 36–42 | PresenceBadge tooltip shows "Active / Busy / Away"; PresencePicker options read "Online / Idle / Do Not Disturb / Invisible" — a DND user shows tooltip "Busy", not "Do Not Disturb" |
Within the same Lotus feature set the user-facing vocabulary is inconsistent between the setter UI and the reader tooltip |
| N20 | Notification Presets | Notifications.tsx |
57–107 | Gaming/Work/Sleep preset buttons are bare <button> elements with Lotus-specific CSS vars (--border-interactive-normal, --bg-surface-low) not defined in all themes |
Grouped preset/action buttons elsewhere use folds Chip variant="Primary/Secondary" outlined radii="Pill" (e.g., Composer Toolbar toggles in General.tsx:1100–1113) |
| N21 | Notification Sound Selects | SystemNotification.tsx |
111–305 | Message sound, invite sound, and quiet-hours time pickers are bare <select>/<input type="time"> with colorScheme: 'dark' workaround |
All other dropdowns in settings use the Button+PopOut+Menu+MenuItem folds pattern; the native select renders OS-styled on all platforms |
| N22 | DM Preview Virtualizer | RoomNavItem.tsx / Direct.tsx |
608–627 / 232 | DM preview adds a second text row to each DM item, making it taller than 38px, but useVirtualizer in Direct.tsx still uses estimateSize: () => 38 — causes layout jump/overlap on initial render |
Non-DM rooms in Home.tsx also estimate 38px; DM items with a preview are now a different height, creating two visual densities in the same nav column |
| N23 | RoomServerACL | RoomServerACL.tsx |
100–115 / 298–309 | Server-name text input is a raw <input type="text"> with inline style object; "Allow IP literal addresses" is a raw <input type="checkbox"> with style={{ width: 16, height: 16 }} |
All other text/boolean controls in room settings use folds Input and Checkbox components (RoomAddress.tsx:163, RoomAddress.tsx:330) |
| N24 | PolicyListViewer | PolicyListViewer.tsx |
245–264 | Room-ID add input is a raw <input type="text"> with manually replicated folds token values |
Native pattern: folds <Input variant="Secondary" size="300" radii="300"> — no inline style needed |
| N25 | ExportRoomHistory Inputs | ExportRoomHistory.tsx |
258–292 | Both date range pickers are raw <input type="date"> with inline styles |
Native pattern: folds Input component; <input type="date"> renders OS-native date picker, unstyled relative to the rest of the settings panel |
| N26 | RoomShareInvite QR | RoomShareInvite.tsx |
66–73 | QR code <img> has no onError handler and no loading state — broken-image placeholder shown when the external API is unreachable |
Cinny avatar components and MediaGallery use onError handlers; this is the only settings element making a request to a third-party server with no graceful degradation |
🟡 Minor — Cosmetic / Token Discipline
| # | Area | File | Lines | Issue | Native Pattern |
|---|---|---|---|---|---|
| N27 | GIF Picker | GifPicker.tsx |
103–110 | FocusTrap omits returnFocusOnDeactivate: false — focus returns to GIF button on dismiss instead of staying in the editor |
EmojiBoard in RoomInput.tsx:978 explicitly sets returnFocusOnDeactivate={false}; GIF picker dismiss behaviour is inconsistent with emoji picker |
| N28 | Character Counter | RoomInput.tsx |
1159–1174 | Composer character counter rendered with color: 'var(--tc-surface-low)' and raw pixel padding — a CSS variable not used anywhere else in the codebase — FIXED: removed undefined var and raw opacity; now <Text priority="300"> with config.space.S100 padding |
Use color.* folds tokens or priority="300" on a Text component |
| N29 | PollCreator Modal | PollCreator.tsx |
103–116 | Modal root is <Box as="form" role="dialog" aria-modal="true"> with manually assembled surface styles instead of folds <Dialog variant="Surface"> |
MessageDeleteItem and MessageReportItem in Message.tsx:506,635 use <Dialog variant="Surface"> inside OverlayCenter > FocusTrap |
| N30 | Playback Speed Chip | AudioContent.tsx |
163–189 | Speed chip uses variant="SurfaceVariant" radii="Pill" while adjacent Play/Pause chip uses variant="Secondary" radii="300" — mismatched shape and variant within the same leftControl row — FIXED: changed speed chip to variant="Secondary" radii="300" |
Controls grouped in the same row should share variant and radii |
| N31 | Collapsible Message Toggle | MsgTypeRenderers.tsx |
97–105 | "Read more ↓" / "Show less ↑" uses <Button size="300" variant="Secondary" fill="None"> — visually a padded form button — FIXED: replaced with the native flush inline-button pattern (background:none;border:none;padding:0) + <Text size="T200"> tinted color.Primary.Main, matching (edited) in FallbackContent |
Inline text toggles in message content (e.g. (edited) in FallbackContent.tsx:74) use bare <button> with background: none; border: none; padding: 0 to stay flush with text |
| N32 | ReadReceiptAvatars Pill | ReadReceiptAvatars.tsx |
95–103 | Pill border is '1px solid rgba(0,212,255,0.30)' hardcoded raw rgba string; borderRadius: '999px' not a folds radii token; padding in raw pixels — FIXED: replaced with config.borderWidth.B300, config.radii.Pill, config.space.S100/S200 |
Use color.* folds tokens and config.radii.Pill / config.space.S* |
ReadReceiptAvatars.tsx |
className="receipt-pill-btn" references a class never defined |
All custom CSS goes through co-located vanilla-extract *.css.ts files |
|||
| N34 | EventReaders Header Size | EventReaders.tsx |
70 | Header size="600" (56px tall) while all peer message-action modals use size="500" (48px) — FIXED: changed to size="500" |
EditHistoryModal, LeaveRoomPrompt, MessageDeleteItem, MessageReportItem all use size="500"; size="600" is reserved for full-page panel headers |
| N35 | EventReaders Close Button | EventReaders.tsx |
96 | Close IconButton missing explicit radii="300" prop — FIXED: added radii="300" |
Every peer modal close button explicitly sets radii="300" (EditHistoryModal:184, LeaveRoomPrompt:75, MessageDeleteItem:517) |
| N36 | EventReaders Header Border | EventReaders.tsx |
72–77 | Lotus-mode header sets borderBottom: '1px solid var(--lt-border-color)' as a CSS shorthand string — FIXED: changed to borderBottomWidth: config.borderWidth.B300 |
Native modals use borderBottomWidth: config.borderWidth.B300 to avoid overriding the border-color set by the folds variant system |
| N37 | EventReaders Timestamp | EventReaders.tsx |
143–151 | Lotus path sets fontSize: '0.72rem' inline — a raw relative unit between folds T200 and T100 scale steps — FIXED: removed raw fontSize, added priority="300" |
Use folds Text size="T200" priority="300" for subdued secondary text |
| N38 | BookmarksPanel Header | BookmarksPanel.tsx |
155–196 | Header uses variant="Surface" and close button uses size="300" radii="300"; also has a SurfaceVariant search bar strip with no equivalent in any native drawer |
MembersDrawer header uses variant="Background" and default-size close button; the extra search+count strip creates a structurally different component family |
| N39 | Forward Menu Icon | Message.tsx |
1150 | Forward context menu item's after icon has no size="100" prop — FIXED: added size="100" to the ArrowRight icon |
Every other after-icon in the same menu block explicitly uses size="100" (Reply, Reaction, Edit, Remind Me, Bookmark); missing size causes the Forward icon to render larger |
| N40 | ProfileDecoration Remove Button | ProfileDecoration.tsx |
185 | "Remove" link is a raw <button> with background: 'none'; color: 'var(--tc-surface-low-contrast)' — an undefined CSS variable — FIXED: replaced with <Button variant="Critical" fill="None" size="300" radii="300"> |
Use folds <Button variant="Critical" fill="None"> or a Text-styled inline link |
| N41 | PresenceBadge / UserNotes Saving | UserRoomProfile.tsx |
240–244 | "Saving…" indicator is <Text opacity={0.5}> without a spinner — FIXED: now shows a folds <Spinner variant="Success" fill="Solid" size="100"> beside the "Saving…" text |
Every other save operation in Profile.tsx shows a folds <Spinner variant="Success" fill="Solid" size="300"> alongside the save button |
| N42 | Character Counter Convention | UserRoomProfile.tsx vs Profile.tsx |
243 / 479–490 | UserPrivateNotes shows remaining count "N left", appears only under 100; ProfileStatus shows "current / 64" always with color progression |
Two Lotus features in the same settings flow use different counter conventions; neither matches a pre-existing Cinny pattern |
| N43 | Night Light Slider | General.tsx |
554–565 | Night Light intensity slider is a raw <input type="range"> with no accentColor token — renders in browser-default blue on all themes — FIXED: added accentColor: color.Primary.Main; the intensity label opacity hack also replaced with priority="300" |
The Gate Threshold slider at General.tsx:1456 at minimum sets accentColor: 'var(--accent-orange)'; the Night Light slider does neither |
| N44 | Mention Highlight & Boot Button | General.tsx |
597–677 | <input type="color"> for mention highlight uses raw pixel dimensions (width: '36px', height: '28px', borderRadius: '4px'); Reset and Boot buttons are bare <button> with Lotus CSS vars — PARTIALLY FIXED: the mention-highlight Reset button (renders on all themes) is now a folds <Button variant="Secondary" fill="Soft">, removing the undefined --border-interactive-normal var. The Boot button is deliberately kept as-is: it only renders when lotusTerminal is active, i.e. exactly when the --accent-orange* TDS vars are defined. The <input type="color"> itself is tracked separately as N69. |
Adjacent settings controls use folds IconButton/Button; there is no other <input type="color"> in the Cinny settings UI |
| N45 | SettingsSelect vs SelectTheme | General.tsx |
126 vs 197 | SettingsSelect trigger uses variant="Secondary" while SelectTheme uses variant="Primary" outlined fill="Soft" for the same Button+PopOut dropdown pattern — adjacent rows in the same Appearance section have different visual weight — FIXED: SelectTheme trigger changed to variant="Secondary" to match SettingsSelect |
Dropdown triggers should share the same variant within the same settings section |
| N46 | RoomInsights SectionHeader | RoomInsights.tsx |
24–37 | SectionHeader adds textTransform: 'uppercase', letterSpacing: '0.06em', opacity: 0.6 to Text size="L400" — FIXED: simplified to <Text size="L400" priority="300"> |
Every other settings panel uses bare <Text size="L400">Label</Text> with no transforms (General.tsx:52–72, ExportRoomHistory.tsx:220,246) |
| N47 | RoomInsights Chart Radii | RoomInsights.tsx |
350–356 / 415–436 | Bar chart uses borderRadius: 3 and histogram bars use borderRadius: '2px 2px 0 0' as raw pixel integers — FIXED: replaced with config.radii.R300 |
All other rounded corners use config.radii.* tokens |
| N48 | RoomInsights Font Size | RoomInsights.tsx |
448 | Hour-axis labels set style={{ fontSize: 9 }} as a raw pixel integer — overrides the folds Text size="T200" applied on the same element — FIXED: removed raw style={{ fontSize: 9 }} |
Use only folds Text size props; never override with raw fontSize |
| N49 | RoomInsights Emoji Icons | RoomInsights.tsx |
41–65 / 292–295 | StatTile uses literal Unicode emoji (🖼️ 🎬 🎵 📎) in <Text size="H4"> as icons — FIXED: StatTile now takes an icon: IconSrc and renders <Icon> using Icons.Photo/VideoCamera/Headphone/File |
All other iconographic elements use <Icon src={Icons.*} /> from folds — emoji rendering varies between Windows/macOS/Linux and cannot be tinted by the theme |
| N50 | RoomInsights Warning Banner | RoomInsights.tsx |
168–192 | Disclaimer banner uses raw <Box style={{ border: color.Warning.Main, background: color.Warning.Container }}> — FIXED: replaced with <SequenceCard variant="SurfaceVariant"> with <Icon> colored via color.Warning.Main |
Settings panel informational cards use <SequenceCard variant="SurfaceVariant"> throughout RoomServerACL, ExportRoomHistory, PolicyListViewer |
| N51 | ExportRoomHistory Progress | ExportRoomHistory.tsx |
311–314 | Export progress shows as a plain Text string ("Exporting… N messages") — WON'T FIX (deliberate): unlike BackupRestore (which has a known total to drive a determinate ProgressBar), export has no known total — it counts messages as they stream. The operation already shows a folds Spinner in the button plus a live count, which is the correct affordance for an indeterminate task. |
BackupRestore.tsx:72,90 uses a folds <ProgressBar variant="Secondary" size="300"> for the same kind of long async operation |
| N52 | MessageQuickReactions Empty Return | Message.tsx |
160 | if (recentEmojis.length === 0) return <span />; — injects an invisible DOM node into the hover action bar flex container — FIXED: changed to return null |
Universal convention for empty renders in Cinny is return null; 144+ instances across the codebase; the empty <span> can affect flex spacing |
Round 2 — Additional Feature Areas
🔴 Additional Major Findings
N53 — PTT Badge (Lotus Terminal path): Raw <div> tree with --lt-* CSS vars instead of folds <Chip>
- File:
src/app/features/call/CallControls.tsx, lines 242–282 - Status: OPEN
- Issue: When
lotusTerminalis true the PTT badge renders as a bare<Box>with inline styles referencing--lt-accent-green-dim,--lt-accent-green-border,--lt-accent-green— variables absent outside TDS mode — hardcoded rem padding,borderRadius: '99px'(non-token), a raw monospacefontFamilystring, non-tokenletterSpacing, and a rawanimation:CSS string for the live-pulse dot. The live●dot is a raw<span>with inline style. - Root Cause: Two entirely separate component trees for the same badge depending on a theme boolean. The non-terminal path (lines 284–301) uses the correct
<Chip variant="Success"|"Warning" fill="Soft" radii="400" outlined>. - Fix: Remove the terminal branch. The standard
<Chip>path already exists and TDS theming can be applied via the CSS variable layer without a separate component tree.
N54 — PiP Mute Overlay Badges: Raw <div> instead of folds <Badge>/<Chip>
- File:
src/app/components/CallEmbedProvider.tsx, lines 438–477 - Status: FIXED — replaced hardcoded
borderRadius/padding/fontSizewithconfig.radii.R300,config.space.S100/S200tokens; replaced raw<span>text with folds<Text size="T200">; color now applied to theIcon/Textviacolor.Critical/Warning.Main. The dark translucent scrim (rgba(0,0,0,0.65)) is deliberately retained: these badges overlay arbitrary video, where a themeChip/Badgesurface token would not guarantee legibility. They are also non-interactive (pointerEvents: 'none'), so an interactiveChip(a<button>) is semantically wrong. - Issue: Both the "You muted" (bottom-left) and "All muted" (top-right) PiP badges are raw
<div>elements with hardcodedbackground: 'rgba(0,0,0,0.65)',backdropFilter: 'blur(4px)',borderRadius: '6px',padding: '3px 7px',fontSize: '12px'. Color is set ascolor: color.Critical.Maindirectly on the wrapper<div>, not via a foldsvariantprop. Text is<span style={{ fontSize: '11px', fontWeight: 600 }}>. - Root Cause:
CallView.tsxline 127 uses<Badge variant="Critical" fill="Solid" size="400">in the same file for the "N Live" indicator — the native pattern exists and is unused here.
N55 — Chat Background / Seasonal Theme Selected State Uses color.Critical.Main (Error Red)
- File:
src/app/features/settings/general/General.tsx, lines 1660–1661 and 1726–1728 - Status: FIXED — replaced all 4 instances of
color.Critical.Mainwithcolor.Primary.MaininGeneral.tsx - Issue: The selected-state border for both
ChatBgGridandSeasonalBgGridisborder: \2px solid ${color.Critical.Main}`and the label color is alsocolor.Critical.Main.color.Critical.Main` is the semantic token for destructive/error states — it is used for "Leave Room", "Delete Message", "Report Room" in the same file. A normal selection indicator rendered in error red is semantically wrong and visually alarming. - Root Cause: Wrong semantic token for an active/selected state.
- Fix: Replace
color.Critical.Mainwithcolor.Primary.Main(orcolor.Success.Mainto match how other settings selections are styled) for both the border and label color.
N56 — Report Modal Category Dropdown: Native <select> Instead of folds Chip+PopOut+Menu
- File:
src/app/features/room/ReportRoomModal.tsxlines 138–163;src/app/features/room/ReportUserModal.tsxlines 144–169 - Status: OPEN
- Issue: Both report modals render the "Category" field as
<Box as="select">with hand-rolled inline styles (padding, border, background, color, fontSize, fontFamily). No other selector in the message-action modal context uses<select>— the established pattern for all dropdowns in both message modals and search filters isChip onClick → setMenuAnchor → PopOut → FocusTrap → Menu → MenuItem(reference:OrderButtoninSearchFilters.tsxlines 63–114). - Fix: Replace native
<select>with<Chip>trigger +<PopOut>+<Menu>+<MenuItem>pattern.
🟠 Additional Moderate Findings
| # | Area | File | Lines | Issue | Native Pattern |
|---|---|---|---|---|---|
| N57 | PiP Fullscreen Button | CallEmbedProvider.tsx |
929–951 | PiP fullscreen toggle is a raw <button> with background: 'rgba(0,0,0,0.65)', color: '#fff', fontSize: '13px', Unicode ⛶/⊡ glyph — no focus ring, no tooltip — FIXED (token discipline): borderRadius/padding/gap replaced with config.radii.R300 + config.space.* tokens (also on the "Return to call" label). The dark scrim and #fff text are deliberately kept for legibility over arbitrary video; the glyph stays because folds has no fullscreen icon. aria-label/title tooltip already present. |
Controls.tsx fullscreen button uses <IconButton variant="Surface" fill="Soft" radii="400" size="400" outlined> with <TooltipProvider>; hardcoded #fff fails on light themes |
| N58 | Screenshare Confirm Popup | CallControls.tsx |
303–360 | "Share your screen?" popup is a raw <Box> with --bg-surface/--bg-surface-border vars (undefined outside TDS), borderRadius: '0.75rem', boxShadow: '0 8px 32px rgba(...)', no FocusTrap |
Cinny's confirmation dialogs use folds <Menu> + <FocusTrap> + <PopOut>; the non-FocusTrap popup is not keyboard-accessible |
| N59 | ML Noise Suppression Panel | General.tsx |
1303–1487 | Sub-panel uses var(--border-color), var(--bg-card), var(--bg-input) (undefined in folds default theme), raw <details>/<summary> (UA-styled), accentColor: 'var(--accent-orange)' (TDS-only) |
All other settings sub-sections use <SettingTile> rows inside <SequenceCard>; no other settings component uses <details> |
| N60 | Knock Badge on Members Button | RoomViewHeader.tsx |
744–782 | Knock count badge wrapped in extra <div style={{ position: 'relative' }}> with hardcoded fontSize: '9px', minWidth: '14px', height: '14px', padding: '0 3px' overriding folds size="200" — FIXED: removed wrapper div, put position: 'relative' directly on the IconButton, <Badge size="400"> with toRem(3) insets and <Text size="L400"> — now matches the Pinned Messages badge pattern exactly |
Pinned Messages badge (same header, lines 651–677) uses position: 'relative' directly on <IconButton> + toRem() for inset; no extra wrapper div |
| N61 | Knock Member Rows | MembersDrawer.tsx |
441–487 | Knock requester rows use raw <Box> with manually duplicated padding; no <MenuItem> wrapper → no hover/focus/active states — WON'T FIX (deliberate): unlike a MemberItem (a clickable navigation row), a knock row contains two action buttons (Approve / Deny) and is not itself clickable. Wrapping it in <MenuItem> (a <button>) would nest interactive controls inside a button — invalid HTML/ARIA. The row has no interactive state to express. |
Every joined/invited member uses <MemberItem> which wraps <MenuItem variant="Background" radii="400"> with baked-in spacing and all interactive states |
| N62 | Unverified Device Banner | RoomInput.tsx |
860–883 | Warning callout above composer uses inline background: color.Warning.Container, borderLeft: '3px solid color.Warning.Main' — a custom left-border accent pattern not present anywhere else in the folds system — FIXED: replaced the borderLeft: '3px' accent with a standard full border using color.Warning.ContainerLine + config.borderWidth.B300; removed the opacity hacks (folds OnContainer already meets contrast) |
Warning indicators in the same codebase use <Chip variant="Warning"> or <Badge variant="Warning">; the 3px left-border card pattern has no folds equivalent |
| N63 | Report Modals — Box Instead of Dialog | ReportRoomModal.tsx / ReportUserModal.tsx |
97–110 / 103–116 | Both modals render as <Box as="form" role="dialog"> with inline background/borderRadius/boxShadow; use config.radii.R400 (rounder) vs native Dialog which uses R300 |
Native MessageReportItem at Message.tsx:634 and all other Cinny message-action modals use <Dialog variant="Surface"> |
| N64 | EditHistoryModal — <Modal> vs <Dialog> |
EditHistoryModal.tsx |
166 | Uses <Modal variant="Surface" size="500"> while sibling message-action modals (DeleteMessageItem:505, MessageReportItem:634) all use <Dialog variant="Surface"> — different widths and internal padding |
<Dialog variant="Surface"> is the established modal shell for all message-triggered dialogs |
| N65 | EditHistoryModal — No "Load More" | EditHistoryModal.tsx |
253–259 | When hasMore is true the modal shows passive <Text>"Showing the 50 most recent edits"</Text> with no action; older edits are inaccessible — FIXED: implemented real pagination — edits accumulate across next_batch fetches (de-duped by event id, re-sorted by ts), with a folds <Button>Load more</Button> (spinner while loading) replacing the passive text |
RoomActivityLog.tsx:425 and MessageSearch.tsx:129 both render a folds <Button size="300" variant="Secondary">Load more</Button> to fetch the next page |
| N66 | DateRangeButton — Native <input type="date"> |
SearchFilters.tsx |
558–589 | "From" and "To" date fields are raw <input type="date"> with inline style overrides including fontSize: '0.82rem' — FIXED: replaced both with folds <Input type="date" variant="SurfaceVariant" size="300" radii="300">; removed now-unused color import |
SelectRoomButton (same file, line 224) and SelectSenderButton (line 424) both use folds <Input size="300" radii="300">; the date inputs are the only native browser inputs in the search filter row |
| N67 | SeasonalEffect / NightLight Z-Index Order | SeasonalEffect.tsx / App.tsx |
759 / 62–77 | SeasonalEffect mounts at zIndex: 9999; NightLightOverlay at zIndex: 9998. Seasonal particles render above Night Light so they are never tinted. SeasonalEffect also shares z-index: 9999 with the skip-to-content link in ClientLayout.tsx — FIXED: lowered SeasonalEffect overlay to zIndex: 9997 (below Night Light at 9998 and modals at 9999), so Night Light now tints the particles and dialogs are never obscured |
Expected UX: Night Light tints all visible content including effects; requires either a higher Night Light z-index or a lower SeasonalEffect z-index |
| N68 | Syntax Highlighting — --lt-accent-* Vars in Non-TDS Themes |
syntaxHighlight.ts |
313–323 | tokenStyle() returns var(--lt-accent-cyan/green/orange/purple, hardcoded-fallback) — --lt-* vars only exist in TDS mode; fallbacks are Monokai dark colors that have poor contrast on light themes and no relationship to the existing --prism-* variables in ReactPrism.css |
ReactPrism.css uses --prism-keyword, --prism-selector etc. which switch correctly between light and dark palettes; syntax highlighting should use the same variable family |
| N69 | Mention Highlight — <input type="color"> Instead of HexColorPickerPopOut |
General.tsx |
644–675 | Raw <input type="color"> with hardcoded pixel dimensions; OS-native color picker chrome renders completely differently from the rest of settings UI |
PowersEditor.tsx:125–143 establishes <HexColorPickerPopOut picker={<HexColorPicker ...>}> as the codebase's color-picking pattern; Reset button should be <Button size="300" variant="Secondary" radii="300"> |
| N70 | ChatBgGrid / SeasonalBgGrid — Raw <button> Elements |
General.tsx |
1648–1689 / 1711–1742 | Both pickers use raw HTML <button> elements with hardcoded width: toRem(76), height: toRem(50/56), borderRadius: toRem(8), border: 2px solid rgba(...) — no focus ring via folds, no variant prop, no hover state from the design system |
Native Cinny theme pickers use folds <MenuItem> or <Chip> which respond to theme and provide focus/hover states automatically |
🟡 Additional Minor Findings
| # | Area | File | Lines | Issue | Native Pattern |
|---|---|---|---|---|---|
| N71 | Call Prescreen Text | CallView.tsx |
63–85 | ChannelFullMessage and AlreadyInCallMessage use <Text style={{ color: color.Critical/Warning.Main }}> inline instead of folds <Badge variant="Critical/Warning"> — WON'T FIX (deliberate): these are full, centered explanatory sentences ("Channel Full (N/M) — Wait for someone to leave…"), not short labels. A Badge is for compact chips like "N Live"; wrapping a sentence in one is visually wrong. They already use folds color.* tokens. The sibling LivekitServerMissingMessage/NoPermissionMessage use the same (un-flagged) pattern. |
The "N Live" badge directly above (line 127) correctly uses <Badge variant="Critical" fill="Solid" size="400"> |
| N72 | Mute MenuItem Icon | RoomNavItem.tsx |
454–466 | "Mute" <MenuItem> places bell-mute icon as a raw child node instead of using the before prop — FIXED: moved Icons.BellMute to before prop |
Every other <MenuItem> in both RoomNavItemMenu and RoomMenu places its leading icon in the before prop |
| N73 | Pending Requests Header | MembersDrawer.tsx |
415–422 | "Pending Requests" section header is bare <Text> with inline padding instead of className={css.MembersGroupLabel} — FIXED: now uses className={css.MembersGroupLabel} like every other section header |
Power-level group labels at lines 506–519 use className={css.MembersGroupLabel} for all other section headers in the same virtualizer list |
| N74 | Emoji Prefix Span | RoomNavItem.tsx |
730–736 | Emoji prefix rendered as raw <span style={{ fontSize: '1.15em', lineHeight: 1 }}> inside a <Text> node — FIXED: removed the emoji-splitting span; the room name (including any leading emoji) now renders directly inside <Text> |
All other nav item text uses folds <Text size="Inherit"> or similar — no raw <span> with em-based font-size override exists elsewhere in the sidebar |
| N75 | Room Name Override / Star Indicators | RoomNavItem.tsx |
741–757 | Pencil and star indicator icons are embedded inside the name <Box as="span">, giving them the same visual baseline as the room name text — WON'T FIX (deliberate): an inline favorite-star / local-name marker adjacent to the name is a deliberate, common design (cf. Element/Slack pinned-name markers). Moving them to the far right would collide with the unread/notification indicators already there and risks layout regressions. Low value, real regression risk. |
Native sidebar status indicators (unread count, notification mode icon) are placed to the far right of the item, never inside the name text span group |
| N76 | Report Modals — Extra Cancel Button | ReportRoomModal.tsx / ReportUserModal.tsx |
189–191 / 195–197 | Both custom report modals include a "Cancel" <Button> in the footer row |
Native MessageReportItem (Message.tsx:675–691) has no Cancel button — dismissal is via × header button or click-outside only |
| N77 | Search Filter Inline Lambdas | SearchFilters.tsx |
480, 625 | SelectSenderButton and DateRangeButton trigger chips use inline onClick arrow functions — WON'T FIX (deliberate): purely a code-style nit with zero user-facing or behavioural impact. Inline arrow handlers are idiomatic React and used throughout this very file; extracting them yields no functional benefit. |
OrderButton (line 58) and SelectRoomButton (line 195) both extract a named const handleOpenMenu: MouseEventHandler<HTMLButtonElement> handler — bypassing the type annotation in the inline form |
| N78 | HasLink Chip Active Color | SearchFilters.tsx |
755 | HasLink active state uses variant="Primary" (blue); all boolean scope-toggle chips in the same bar use variant="Success" (green) with outlined — FIXED: changed to variant={containsUrl ? 'Success' : 'SurfaceVariant'} outlined={!!containsUrl} |
variant="Success" outlined is the established active-state pattern for boolean toggles in the filter bar |
| N79 | Server Notice Chip Radii | RoomViewHeader.tsx |
570 | <Chip size="400" radii="Pill"> — Pill radii on a room-type label — FIXED: changed to radii="300" |
Room/space type labels in lobby (RoomItem.tsx:83, SpaceItem.tsx:63) use radii="300"; radii="Pill" is for filter/tag chips only |
| N80 | Server Support Contact Layout | About.tsx |
172–239 | Homeserver support contacts rendered as raw <Box direction="Column"> with <Text as="a"> pairs — custom label/link layout |
All other <SequenceCard> content in About.tsx and General.tsx uses <SettingTile title="..." description="..." after={...}> as the content unit |
| N81 | Background Picker Grid — No Responsive Layout | General.tsx |
1707–1742 | Fixed width: toRem(76) flex-wrap cells with no minWidth floor or CSS grid auto-fill — SeasonalBgGrid's 13 items produce a visually lopsided orphan last row at any viewport width |
Cinny's native grids use grid-template-columns: repeat(auto-fill, minmax(N, 1fr)) or equivalent for responsive fill |
| N82 | Join/Leave Sounds Auto-Preview | General.tsx |
1592–1609 | Selecting a sound in the dropdown immediately plays a preview, but no UI affordance (button label, description text, or "▶ Preview" button) communicates this to the user | Settings tiles with side effects on selection (theme picker, chat background) show a live visual preview or a dedicated control explaining the side effect |
Round 3 — Rich Topic Editor, RemindMe Dialog, Composer Toolbar, Voice Recorder, Uploads, Location, Mention Highlight
🔴 Additional Major Findings
N83 — Rich Topic Formatting Toolbar: Raw <button> Elements with Fully Inline Styles
- File:
src/app/features/common-settings/general/RoomProfile.tsx, lines 335–358 - Status: FIXED — replaced raw
<button>elements with<Button size="300" radii="300" variant="Secondary" fill="Soft">with styled<Text>children for B/I/S/code labels - Issue: The four formatting buttons (B, I, S,
`) in the room topic editor are plain HTML<button>elements with entirely inline styles: manualborder,borderRadius,background,color,cursor,fontSize,fontWeight,fontStyle,fontFamily,lineHeight. They bypass the folds design token system completely — novariant,size, orradiiprops, no theme-reactive hover/focus states. - Root Cause: Custom addition without referencing folds primitives.
- Fix: Replace with
<IconButton type="button" size="300" radii="300" variant="Surface" fill="Soft">matching the emoji-picker trigger immediately above them at line 285, which already uses the correct pattern.
N84 — Topic Preview in Room Settings Renders Plain Text Instead of formatted_body
- File:
src/app/features/common-settings/general/RoomProfile.tsx, lines 457–461 - Status: FIXED — read-mode topic now checks
topic.format === 'org.matrix.custom.html'and rendersparse(sanitizeCustomHtml(topic.formatted_body)), matchingRoomTopicViewerand all other display sites - Issue: The read-mode topic display wraps
topic.topic(the plain-text field) in<Linkify>and never readsformatted_body. HoweverbuildTopicContent()(lines 82–89) intentionally stores bothtopicandformatted_bodyunderorg.matrix.custom.html. After the user saves a formatted topic, the preview panel immediately shows the stripped plain-text version — the formatting appears to disappear within the same settings panel. - Root Cause: The existing
RoomTopicViewercomponent (src/app/components/room-topic-viewer/RoomTopicViewer.tsx:24–51) already checkstopic.format === 'org.matrix.custom.html'and pipesformatted_bodythroughsanitizeCustomHtml. This component is used everywhere else (RoomIntro,LobbyHero,RoomItem,Invites, etc.) but not in Room Settings. - Fix: Replace the inline plain-text render with
<RoomTopicViewer topic={roomTopic}>to match all other display sites.
🟠 Additional Moderate Findings
| # | Area | File | Lines | Issue | Native Pattern |
|---|---|---|---|---|---|
| N85 | RemindMe Dialog Shell | RemindMeDialog.tsx |
69–81 | Dialog shell is <Box role="dialog"> with background, borderRadius, boxShadow, overflow all set as inline styles using token lookups. Corner radius is config.radii.R400 which differs from the R300 embedded in <Dialog variant="Surface"> — FIXED: shell replaced with <Dialog variant="Surface" style={modalStyle}>; removed the inline background/borderRadius/boxShadow/overflow and the now-unused color import |
All small message-action dialogs (LeaveRoomPrompt, LogoutDialog, JoinAddressPrompt, PowerChip, DeleteMessageItem) use <Dialog variant="Surface" style={modalStyle}> as the shell |
| N86 | RemindMe Preset Buttons | RemindMeDialog.tsx |
111–117 | The four preset time choices (20 min, 1 hr, 3 hr, tomorrow) use <MenuItem size="300" radii="300"> — MenuItem is a navigation primitive tied to menu/menubar ARIA roles; placing it inside role="dialog" is an invalid ARIA combination — FIXED: each preset is now a folds <Button variant="Secondary" fill="Soft" radii="300">, resolving the invalid menuitem-in-dialog ARIA |
Dialog action choices use <Button> (delete/leave/logout dialogs) or <Chip> (selection choices). No other dialog in the codebase uses MenuItem for action items |
| N87 | Composer Toolbar Toggle Pattern | General.tsx |
1100–1114 | Per-button toolbar toggles (Format, Emoji, Sticker, GIF, Location, Poll, Voice, Schedule) use <Chip variant="Primary"/"Secondary" radii="Pill"> in a wrap grid — a compact chip-toggle grid inside a SettingTile, different from every adjacent row |
The three sibling tiles in the same Editor() function (ENTER for Newline, Markdown, Formatting Toolbar) all use <SettingTile after={<Switch variant="Primary">}>. 15+ other binary settings in the file use the Switch pattern |
| N88 | Voice Recorder Recording State | VoiceMessageRecorder.tsx |
195, 206, 240, 276 | Recording container background is var(--bg-surface-variant), the live pulse dot is var(--tc-danger-normal), waveform bars are var(--tc-primary-normal) — custom Lotus CSS vars that may not exist in folds themes, falling back to transparent/black — FIXED: replaced with color.SurfaceVariant.Container, color.Critical.Main, color.Primary.Main |
Native message components use JS-accessible color.* tokens that are always populated regardless of theme class |
| N89 | Voice Recorder Preview Audio | VoiceMessageRecorder.tsx |
282–283 | Preview state renders bare <audio src={previewUrl} controls> — native browser element with inconsistent cross-browser chrome — FIXED: replaced with <audio ref> + folds <IconButton> play/pause toggle; onEnded resets playing state |
Native audio messages use folds Attachment/AttachmentContent layout wrappers; pre-send preview should use <IconButton> play/pause controls |
| N90 | Mention Highlight Contrast Formula | App.tsx |
36–40 | Auto-computed text color (black/white) uses simplified luma (0.299r + 0.587g + 0.114b)/255 > 0.5 — not WCAG 2.1 relative luminance (which requires gamma linearization) — FIXED: replaced with WCAG 2.1 relative luminance formula using ((c+0.055)/1.055)^2.4 gamma linearization; threshold moved from 0.5 to 0.179 |
Folds color.*.OnContainer tokens are manually curated to pass WCAG AA 4.5:1 contrast ratios; custom computation must match this guarantee |
🟡 Additional Minor Findings
| # | Area | File | Lines | Issue | Native Pattern |
|---|---|---|---|---|---|
| N91 | Upload Card Caption Input | UploadCardRenderer.tsx |
356–376 | Caption input is raw <input type="text"> with hardcoded inline CSS using Lotus-specific vars not in folds — FIXED: replaced with folds <Input variant="Secondary" size="300" radii="300"> |
Other text inputs in the UI use folds <Input size="300" radii="300"> with folds-token props for all sizing and color |
| N92 | Location "Open Location" Button | MsgTypeRenderers.tsx |
534–547 | "Open Location" action link uses <Chip as="a"> — compact badge-sized element — FIXED: replaced with <Button as="a" variant="Secondary" fill="Solid" radii="300" size="400"> matching FileContent pattern |
FileContent.tsx uses <Button variant="Secondary" fill="Solid" radii="300" size="400"> for "Open File"/"Open PDF" |
| N93 | Location Coordinates Text | MsgTypeRenderers.tsx |
532 | <Text size="T300" style={{ opacity: 0.65 }}> — hardcoded non-standard opacity — FIXED: replaced with <Text size="T300" priority="300"> |
Secondary text uses folds priority prop; 0.65 is outside the token scale |
| N94 | Mention Highlight Border Invisible | App.tsx |
41 | --mention-highlight-border is set to the same value as --mention-highlight-bg — the border is invisible — FIXED: border is now rgba(r,g,b,0.5) — same hue as the background at 50% opacity, always visible |
In folds, color.*.ContainerLine is always a lighter/muted sibling of color.*.Container, providing the 1px outline that gives mention chips visual definition |