From c395f7d16e5dd4180319cb290e69d517485f9d2b Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 18 Jun 2026 13:34:40 -0400 Subject: [PATCH] fix(mobile): touch targets, keyboard viewport, PageNav overflow, modal fullscreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bug #10: use `100dvh` on so layout shrinks when mobile virtual keyboard appears (prevents composer from being pushed off-screen) - Bug #7: add `minWidth/minHeight: 44px` to all 8 composer toolbar IconButtons on mobile via mobileOrTablet() check (WCAG 2.1 AA touch target requirement) - Bug #8: add `@media (max-width: 750px) { width: 100% }` to PageNav recipe variants so the nav panel fills full width on mobile instead of overflowing with its fixed desktop width - Bug #9: introduce `useModalStyle(maxWidth)` hook — returns fullscreen styles on mobile (no border-radius, no max-width cap, height 100%) and desktop box styles otherwise; applied to LeaveRoomPrompt, LeaveSpacePrompt, ReportRoomModal, ReportUserModal - Bug #11: mark as FALSE POSITIVE in LOTUS_BUGS.md — `useState(() => atom(...))` is the correct Jotai pattern for stable local atom references - Scheduled Messages persistence: mark as FIXED — already uses atomWithStorage + createJSONStorage with error-safe JSON parsing - UrlPreviewCard TDS colors: mark as BRAND EXCEPTION — SVG logo fills and site badge backgrounds are official third-party brand colors; cannot convert without inventing new CSS variables (violates TDS rule 3) Co-Authored-By: Claude Sonnet 4.6 --- LOTUS_BUGS.md | 8 ++--- .../leave-room-prompt/LeaveRoomPrompt.tsx | 4 ++- .../leave-space-prompt/LeaveSpacePrompt.tsx | 4 ++- src/app/components/page/style.css.ts | 6 ++++ src/app/features/room/ReportRoomModal.tsx | 6 ++-- src/app/features/room/ReportUserModal.tsx | 6 ++-- src/app/features/room/RoomInput.tsx | 10 +++++++ src/app/hooks/useModalStyle.ts | 29 +++++++++++++++++++ src/index.css | 2 ++ 9 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 src/app/hooks/useModalStyle.ts diff --git a/LOTUS_BUGS.md b/LOTUS_BUGS.md index e0755272f..be7b3f696 100644 --- a/LOTUS_BUGS.md +++ b/LOTUS_BUGS.md @@ -100,9 +100,9 @@ This document tracks identified bugs, edge cases, and architectural discrepancie ### 11. Inline Jotai atom creation - **File:** `cinny/src/app/hooks/useSpaceHierarchy.ts` -- **Status:** **OPEN** +- **Status:** **FALSE POSITIVE — CLOSED** - **Issue:** Inline Jotai atom creation in a hook risks re-rendering components unnecessarily. -- **Proposed Fix:** Lift atom definition out of the hook or utilize `useMemo` to ensure atom stability. +- **Resolution:** `useState(() => atom(...))` IS the correct Jotai pattern for local stable atom references. The factory function form of `useState` ensures the atom is created only once per component mount. No change warranted. --- @@ -123,7 +123,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie | 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` | OPEN | -| Data Persistence | Scheduled Messages are ephemeral (lost on refresh) due to fragile `localStorage` parsing. | `cinny/src/app/state/scheduledMessages.ts` | 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` | OPEN | | 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 | @@ -150,7 +150,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie | :-------------------------------------------------------------------- | :-------------------------------------------------------- | | 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` | `cinny/src/app/components/url-preview/UrlPreviewCard.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 | `cinny/src/app/features/lotus/chatBackground.ts` | | 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` | diff --git a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx index 714ee4836..f4c63cde3 100644 --- a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx +++ b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx @@ -20,6 +20,7 @@ import { MatrixError } from 'matrix-js-sdk'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { stopPropagation } from '../../utils/keyboard'; +import { useModalStyle } from '../../hooks/useModalStyle'; type LeaveRoomPromptProps = { roomId: string; @@ -28,6 +29,7 @@ type LeaveRoomPromptProps = { }; export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptProps) { const mx = useMatrixClient(); + const modalStyle = useModalStyle(480); const [leaveState, leaveRoom] = useAsyncCallback( useCallback(async () => { @@ -56,7 +58,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro escapeDeactivates: stopPropagation, }} > - +
( useCallback(async () => { @@ -56,7 +58,7 @@ export function LeaveSpacePrompt({ roomId, onDone, onCancel }: LeaveSpacePromptP escapeDeactivates: stopPropagation, }} > - +
('spam'); const [reportState, submitReport] = useAsyncCallback( @@ -103,9 +105,7 @@ export function ReportRoomModal({ roomId, onClose }: ReportRoomModalProps) { background: color.Surface.Container, borderRadius: config.radii.R400, boxShadow: color.Other.Shadow, - width: '100%', - maxWidth: 420, - overflow: 'hidden', + ...modalStyle, }} >
('spam'); const [reportState, submitReport] = useAsyncCallback( @@ -109,9 +111,7 @@ export function ReportUserModal({ userId, onClose }: ReportUserModalProps) { background: color.Surface.Container, borderRadius: config.radii.R400, boxShadow: color.Other.Shadow, - width: '100%', - maxWidth: 420, - overflow: 'hidden', + ...modalStyle, }} >
( const [toolbar, setToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [composerToolbarButtons] = useSetting(settingsAtom, 'composerToolbarButtons'); + const touchTarget = mobileOrTablet() ? { minWidth: '44px', minHeight: '44px' } : undefined; const showFormat = composerToolbarButtons?.showFormat ?? true; const showEmoji = composerToolbarButtons?.showEmoji ?? true; const showSticker = composerToolbarButtons?.showSticker ?? true; @@ -936,6 +937,7 @@ export const RoomInput = forwardRef( variant="SurfaceVariant" size="300" radii="300" + style={touchTarget} > @@ -947,6 +949,7 @@ export const RoomInput = forwardRef( variant="SurfaceVariant" size="300" radii="300" + style={touchTarget} aria-label={toolbar ? 'Hide formatting toolbar' : 'Show formatting toolbar'} aria-pressed={toolbar} onClick={() => setToolbar(!toolbar)} @@ -998,6 +1001,7 @@ export const RoomInput = forwardRef( variant="SurfaceVariant" size="300" radii="300" + style={touchTarget} > ( variant="SurfaceVariant" size="300" radii="300" + style={touchTarget} > ( size="300" radii="300" disabled={gifUploading} + style={touchTarget} > {gifUploading ? ( @@ -1119,6 +1125,7 @@ export const RoomInput = forwardRef( size="300" radii="300" title="Share location" + style={touchTarget} > {locating ? ( @@ -1135,6 +1142,7 @@ export const RoomInput = forwardRef( size="300" radii="300" title="Create poll" + style={touchTarget} > @@ -1170,6 +1178,7 @@ export const RoomInput = forwardRef( variant="SurfaceVariant" size="300" radii="300" + style={touchTarget} aria-label="Schedule message" title="Schedule message" > @@ -1181,6 +1190,7 @@ export const RoomInput = forwardRef( variant="SurfaceVariant" size="300" radii="300" + style={touchTarget} aria-label="Send message" > diff --git a/src/app/hooks/useModalStyle.ts b/src/app/hooks/useModalStyle.ts new file mode 100644 index 000000000..3bb1b9be9 --- /dev/null +++ b/src/app/hooks/useModalStyle.ts @@ -0,0 +1,29 @@ +import { CSSProperties } from 'react'; +import { ScreenSize, useScreenSizeContext } from './useScreenSize'; + +/** + * Returns responsive modal box styles. On mobile the modal expands to fill the + * full viewport (no border radius, no max-width cap) so it doesn't float as a + * small centered card on a phone screen. + */ +export function useModalStyle(desktopMaxWidth: number): CSSProperties { + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + + if (isMobile) { + return { + width: '100%', + height: '100%', + maxWidth: '100%', + maxHeight: '100%', + borderRadius: 0, + overflow: 'hidden auto', + }; + } + + return { + width: '100%', + maxWidth: desktopMaxWidth, + overflow: 'hidden', + }; +} diff --git a/src/index.css b/src/index.css index a33284952..2c128418d 100644 --- a/src/index.css +++ b/src/index.css @@ -57,6 +57,8 @@ html { height: 100%; + /* dvh shrinks when mobile virtual keyboard appears, so the layout never gets pushed off-screen */ + height: 100dvh; overflow: hidden; }