feat(ui): viewport-fit, safe-area helpers, smoother nav drawer (LC-199) #251

Merged
nrupard merged 2 commits from feat/lc-199-responsive into main 2026-05-28 16:40:33 +02:00
Owner

LC-199 mobile polish, slice 1. The remaining items (44px touch-target audit, swipe-to-close on the drawer, responsive admin tables wrap, full-screen modals at phone widths) stay open under LC-199.

What's shipping

viewport-fit=cover, base.html. Lets the layout extend behind the iOS notch / home indicator. Without this, env(safe-area-inset-*) returns 0 even on a cutout device, so the helper classes below would be no-ops everywhere.

Three safe-area helpers, main.css, all wrapped in max(...) so the existing visual padding is preserved when the inset is 0 (desktop, Android without nav-bar inset, any non-cutout device):

  • .lc-pb-safe for form / bar bottom padding (max(0.5rem, env(safe-area-inset-bottom)))
  • .lc-px-safe for fullscreen content side padding
  • .lc-bottom-safe for fixed-positioned bars anchored to bottom

Smoother nav drawer, main.css + layout.html. Mobile drawer was a binary hidden/flex toggle; now slides in/out on transform: translateX(0/-100%) with a 200ms ease-out transition, and the backdrop fades. Keyed off data-open="true"/"false" instead of class soup. The CSS is @media (max-width: 767px) only so desktop stays exactly as it was. The universal prefers-reduced-motion: reduce reset from LC-196 already short-circuits both transitions to ~0ms, so no extra guard.

Adopted:

  • room/composer.html form: p-2 -> px-2 pt-2 lc-pb-safe. iPhone message bar now clears the home indicator.
  • layout.html outbox banner: bottom-3 -> lc-bottom-safe.

Implementation notes worth flagging

  • The drawer open path does void p.offsetWidth between removing hidden and setting data-open=true. Without that reflow, the browser would coalesce both DOM mutations into the same frame and the transition wouldn't have a starting state to interpolate from; the panel would snap in instead of sliding.
  • The close path does setTimeout(..., 220ms) before re-applying hidden. The 220ms = 200ms transition + 20ms margin. The guard inside the timeout re-checks data-open === 'false' so a fast open-close-open sequence doesn't accidentally hide an already-reopened drawer.

Not in this PR (still open under LC-199)

  • 44px touch-target audit (~9 candidate sites: scheduled / reminders modals, room-header buttons, two-factor / mention popover).
  • Swipe-to-close gesture on the drawer (touchstart/touchend, dx threshold).
  • Responsive admin tables: 12 of 13 admin tables are unwrapped <table class="w-full"> and overflow horizontally below 540ish px. Wrap each in <div class="overflow-x-auto">.
  • Modal full-screen on phone widths. LC-195's .modal-panel is w-full max-w-md; on phone < 28rem viewports it's effectively full-width with p-4 outer padding, so this is cosmetic - dropping the outer p-4 on phone would make modals fully flush.

Tests

just test green (modulo the documented routes_uploads concurrent-binary flake; passes 12/12 isolated). just test-saas green.

🤖 Generated with Claude Code

LC-199 mobile polish, slice 1. The remaining items (44px touch-target audit, swipe-to-close on the drawer, responsive admin tables wrap, full-screen modals at phone widths) stay open under LC-199. ### What's shipping **viewport-fit=cover**, base.html. Lets the layout extend behind the iOS notch / home indicator. Without this, `env(safe-area-inset-*)` returns 0 even on a cutout device, so the helper classes below would be no-ops everywhere. **Three safe-area helpers**, main.css, all wrapped in `max(...)` so the existing visual padding is preserved when the inset is 0 (desktop, Android without nav-bar inset, any non-cutout device): - `.lc-pb-safe` for form / bar bottom padding (`max(0.5rem, env(safe-area-inset-bottom))`) - `.lc-px-safe` for fullscreen content side padding - `.lc-bottom-safe` for fixed-positioned bars anchored to bottom **Smoother nav drawer**, main.css + layout.html. Mobile drawer was a binary `hidden`/`flex` toggle; now slides in/out on `transform: translateX(0/-100%)` with a 200ms ease-out transition, and the backdrop fades. Keyed off `data-open="true"/"false"` instead of class soup. The CSS is `@media (max-width: 767px)` only so desktop stays exactly as it was. The universal `prefers-reduced-motion: reduce` reset from LC-196 already short-circuits both transitions to ~0ms, so no extra guard. **Adopted**: - `room/composer.html` form: `p-2` -> `px-2 pt-2 lc-pb-safe`. iPhone message bar now clears the home indicator. - `layout.html` outbox banner: `bottom-3` -> `lc-bottom-safe`. ### Implementation notes worth flagging - The drawer open path does `void p.offsetWidth` between removing `hidden` and setting `data-open=true`. Without that reflow, the browser would coalesce both DOM mutations into the same frame and the transition wouldn't have a starting state to interpolate from; the panel would snap in instead of sliding. - The close path does `setTimeout(..., 220ms)` before re-applying `hidden`. The 220ms = 200ms transition + 20ms margin. The guard inside the timeout re-checks `data-open === 'false'` so a fast open-close-open sequence doesn't accidentally hide an already-reopened drawer. ### Not in this PR (still open under LC-199) - 44px touch-target audit (~9 candidate sites: scheduled / reminders modals, room-header buttons, two-factor / mention popover). - Swipe-to-close gesture on the drawer (touchstart/touchend, dx threshold). - Responsive admin tables: 12 of 13 admin tables are unwrapped `<table class="w-full">` and overflow horizontally below 540ish px. Wrap each in `<div class="overflow-x-auto">`. - Modal full-screen on phone widths. LC-195's `.modal-panel` is `w-full max-w-md`; on phone < 28rem viewports it's effectively full-width with `p-4` outer padding, so this is cosmetic - dropping the outer `p-4` on phone would make modals fully flush. ### Tests just test green (modulo the documented `routes_uploads` concurrent-binary flake; passes 12/12 isolated). just test-saas green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(ui): viewport-fit, safe-area helpers, smoother nav drawer (LC-199)
Some checks failed
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 5s
Check / clippy + fmt + tests (pull_request) Failing after 25s
77df44fccf
LC-189 polish epic, mobile slice. The remaining LC-199 follow-ups (44px touch-target audit, swipe-to-close on the drawer, responsive admin tables, full-screen modals on phone widths) stay open under LC-199.

base.html: meta viewport gains `,viewport-fit=cover` so the layout extends behind the iOS notch / home indicator. Necessary precondition for the `env(safe-area-inset-*)` lookups to return non-zero values on cutout devices.

assets/main.css: three helper classes wrapping `env(safe-area-inset-*)` inside `max(...)` so the existing visual padding is preserved when the inset is 0 (desktop / non-cutout devices):
- `.lc-pb-safe` - bottom padding for forms / bars (>= 0.5rem, >= inset)
- `.lc-px-safe` - left/right padding for fullscreen content
- `.lc-bottom-safe` - bottom position for fixed bars (push up by inset)

assets/main.css: nav-drawer transition keyed off a `data-open` attribute (mobile only, gated by `@media (max-width: 767px)`). Drawer slides in/out on translateX; backdrop fades. Universal `prefers-reduced-motion: reduce` reset from LC-196 already short-circuits both transitions to ~0ms, so no extra guard needed here.

layout.html `lcOpenNav` / `lcCloseNav`: drop the bare `hidden` toggle in favor of `data-open` + forced reflow (`void p.offsetWidth`) so the slide-in transition kicks off cleanly. Close path waits 220ms for the slide-out transition before re-adding `hidden`. Stays correct if JS or CSS fails (the `hidden` toggle remains the source of truth for "fully gone").

layout.html outbox banner: `bottom-3` -> `lc-bottom-safe` so the offline outbox banner clears the home indicator.

room/composer.html: form padding refactored from `p-2` to `px-2 pt-2 lc-pb-safe` so the message bar respects the safe-area inset on iOS.

just test (excluding the documented routes_uploads concurrent-binary flake; passes 12/12 isolated) and just test-saas both green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(ui): #main uses 100dvh so iOS URL bar doesn't eat content (LC-199)
Some checks failed
check-secrets / TruffleHog (pull_request) Successful in 3s
check-secrets / Nosey parker (pull_request) Successful in 3s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (push) Successful in 6s
check-secrets / Kingfisher (push) Successful in 6s
Check / clippy + fmt + tests (pull_request) Failing after 33s
Create release / Create release from merged PR (pull_request) Has been skipped
2d44ff2e81
Self-review on PR #251 caught it. With the LC-199 `viewport-fit=cover` meta, `height: 100vh` on iOS Safari now includes the URL bar / dynamic-toolbar area: content extends behind the chrome and scrolls underneath. `100dvh` (dynamic viewport height, CSS Values 4) excludes the URL bar - it shrinks/grows as the toolbar shows/hides.

Kept the `height: 100vh` line as a fallback above `100dvh` for browsers that don't yet understand `dvh` (Safari < 15.4, Firefox < 110); the cascade picks the second declaration when supported and ignores it when not.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-199-responsive 2026-05-28 16:40:33 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!251
No description provided.