feat(mobile): Wave 4 — RegisterPage responsive + TransferIndicator on settings + MessageInput sheets

- RegisterPage: scroll envelope, iOS-zoom-safe inputs, ≥44px tap targets, mobile-friendly invite chip + swatch row
- TransferIndicator: mounted via MobileScreenHeader.rightActions across every settings/instance screen and inline in MobileChatScreen; touch-close listener; viewport-safe panel width
- MessageInput popovers (emoji/GIF/mention) now route through new InputPopover wrapper — desktop popover, mobile bottom-sheet
- EmojiPicker mobile branch: dynamicWidth + scoped CSS (.emoji-picker-wrapper--mobile) so the <em-emoji-picker> custom element fills the sheet; bigger touch targets (perLine 8, button 40, emoji 28); desktop unchanged
- MessageInput trigger row: mobile-first sizing with md: desktop overrides (40×40 buttons + gap on mobile, 34×34 unchanged on desktop)
- Spec updates: docs/systems/auth.md (RegisterPage responsive contract), docs/systems/uploads.md (TransferIndicator mobile surfaces)
This commit is contained in:
Jannis Braun
2026-05-07 22:58:57 +02:00
parent 8544f83225
commit fb9fe326c4
13 changed files with 439 additions and 135 deletions
@@ -30,13 +30,21 @@ export function TransferIndicator() {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Click-outside-to-close. iOS Safari does not synthesize `mousedown` from
// touch reliably, so we listen for `touchstart` alongside `mousedown` so the
// panel dismisses on the first tap on touch devices. Mirrors the pattern
// used in AudioInputSection.tsx.
useEffect(() => {
if (!open) return;
const onClick = (e: MouseEvent) => {
const onPointerDown = (e: MouseEvent | TouchEvent) => {
if (!ref.current?.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('mousedown', onClick);
return () => document.removeEventListener('mousedown', onClick);
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('touchstart', onPointerDown, { passive: true });
return () => {
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('touchstart', onPointerDown);
};
}, [open]);
const active = visible.filter(
@@ -69,7 +77,15 @@ export function TransferIndicator() {
)}
</button>
{open && (
<div className="absolute right-0 top-full mt-2 w-[300px] max-w-[calc(100vw-1rem)] glass z-50 rounded-lg overflow-hidden">
// Desktop: anchor below the trigger via `absolute right-0 top-full`.
// Mobile (<768px): switch to `fixed` so the panel pins to the viewport
// right edge with an 8px margin instead of inheriting the trigger's
// x-position. This prevents left-edge clipping on narrow viewports
// (e.g. 320px) where the trigger sits well to the left of the
// viewport's right edge — `right-0` would otherwise anchor the
// panel to the trigger's right and let the 300px panel extend off
// the left of the viewport.
<div className="fixed right-2 top-14 md:absolute md:right-0 md:top-full md:mt-2 w-[min(300px,calc(100vw-16px))] glass z-50 rounded-lg overflow-hidden">
<div className="px-3 py-2 border-b border-border-soft text-xs flex justify-between items-center">
<span className="text-txt-secondary">
{visible.length} transfer{visible.length === 1 ? '' : 's'}