feat(frontend): safe-area insets + hardened reduced-motion

- Add viewport export with viewportFit: "cover" so iOS exposes
  env(safe-area-inset-*) (required for the insets to take effect).
- NavRail / TopBar / main / Toaster now respect safe-area insets so content
  clears the iPhone notch and home indicator in both portrait and landscape.
- prefers-reduced-motion: the media query already disabled declared animation
  classes; harden it with a global transition/animation duration override and
  kill the scan-line shimmer so motion-sensitive users get a fully static UI.

Verified tsc --noEmit + next build clean.
This commit is contained in:
asepharyana
2026-08-16 16:55:34 +07:00
parent c89288191e
commit 0ace758c79
6 changed files with 24 additions and 5 deletions
+12
View File
@@ -262,6 +262,7 @@
@media (prefers-reduced-motion: reduce) {
.scan-line::after,
.scan-line,
.animate-eq,
.animate-spin-disc,
.animate-pulse-ring,
@@ -269,5 +270,16 @@
.animate-breathe {
animation: none !important;
}
.scan-line {
background: none !important;
}
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
html { scroll-behavior: auto; }
}
+8 -1
View File
@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Bricolage_Grotesque, Inter, JetBrains_Mono } from "next/font/google";
import { ThemeProvider } from "next-themes";
import { Toaster } from "@/components/primitives/toast";
@@ -28,6 +28,13 @@ export const metadata: Metadata = {
"AI-powered Discord moderation, voice monitoring, and media control console",
};
export const viewport: Viewport = {
themeColor: "#0a0a0c",
width: "device-width",
initialScale: 1,
viewportFit: "cover",
};
export default function RootLayout({
children,
}: Readonly<{
@@ -57,7 +57,7 @@ export function Toaster({ position = "bottom-right" }: { position?: string }) {
const pos =
position === "bottom-right"
? "bottom-4 right-4"
? "bottom-[calc(1rem+env(safe-area-inset-bottom))] right-[calc(1rem+env(safe-area-inset-right))]"
: position === "top-right"
? "top-4 right-4"
: "bottom-4 left-1/2 -translate-x-1/2";
@@ -12,7 +12,7 @@ export function AppFrame({ children }: { children: React.ReactNode }) {
<NavRail />
<div className="flex min-w-0 flex-1 flex-col">
<TopBar />
<main className="min-h-0 flex-1 overflow-y-auto px-4 pb-8 sm:px-5">
<main className="min-h-0 flex-1 overflow-y-auto px-4 pb-[calc(2rem+env(safe-area-inset-bottom))] sm:px-5">
{children}
</main>
</div>
@@ -40,7 +40,7 @@ export function NavRail() {
const path = pathname ?? "/";
return (
<nav className="glass m-3 mr-0 flex w-[68px] flex-col items-center gap-1 rounded-[18px] py-4">
<nav className="glass mb-[calc(0.75rem+env(safe-area-inset-bottom))] ml-[calc(0.75rem+env(safe-area-inset-left))] mt-[calc(0.75rem+env(safe-area-inset-top))] flex w-[68px] flex-col items-center gap-1 rounded-[18px] py-4">
<div className="flex flex-1 flex-col gap-1">
{navItems.map((item) => (
<NavItem
@@ -32,7 +32,7 @@ export function TopBar() {
: "text-signal";
return (
<header className="sticky top-0 z-40 flex items-center gap-3 px-4 py-3 sm:gap-4 sm:px-5 sm:py-3.5">
<header className="sticky top-0 z-40 flex items-center gap-3 px-4 py-3 pt-[calc(0.75rem+env(safe-area-inset-top))] sm:gap-4 sm:px-5 sm:py-3.5">
<div className="flex min-w-0 items-baseline gap-2 sm:gap-3">
<span className="eyebrow hidden sm:inline">GMW</span>
<h1 className="display truncate text-[1.25rem] text-ink sm:text-[1.5rem]">{label}</h1>