fix: mobile layout — prevent horizontal overflow on notifications page

- AuthGuard: switch outer shell from h-dvh to fixed inset-0 so the
  container always matches the visual viewport regardless of Android
  Chrome address-bar state; add min-w-0 to content column so flex
  children (e.g. long ntfy URL input) cannot force the column wider
  than the viewport
- Notifications page: add overflow-x-auto + shrink-0 to both tab bars
  so button overflow scrolls within the bar instead of escaping to the
  page; add min-w-0 to all inline label/hint div pairs in
  ModeFilterSection and Discovery so they shrink correctly in flex
  layout; add break-all to bill title line-clamp paragraph

Authored by: Jack Levy
This commit is contained in:
Jack Levy
2026-03-15 21:09:23 -04:00
parent 761ffa85d9
commit 7ff75f9a00
2 changed files with 14 additions and 14 deletions

View File

@@ -126,21 +126,21 @@ function ModeFilterSection({
<input type="checkbox" checked={!!filters["new_document"]} <input type="checkbox" checked={!!filters["new_document"]}
onChange={(e) => onChange({ ...filters, new_document: e.target.checked })} onChange={(e) => onChange({ ...filters, new_document: e.target.checked })}
className="mt-0.5 rounded" /> className="mt-0.5 rounded" />
<div><span className="text-sm">New bill text</span><span className="text-xs text-muted-foreground ml-2">The full text of the bill is published</span></div> <div className="min-w-0"><span className="text-sm">New bill text</span><span className="text-xs text-muted-foreground ml-2">The full text of the bill is published</span></div>
</label> </label>
<label className="flex items-start gap-3 py-1.5 cursor-pointer"> <label className="flex items-start gap-3 py-1.5 cursor-pointer">
<input type="checkbox" checked={!!filters["new_amendment"]} <input type="checkbox" checked={!!filters["new_amendment"]}
onChange={(e) => onChange({ ...filters, new_amendment: e.target.checked })} onChange={(e) => onChange({ ...filters, new_amendment: e.target.checked })}
className="mt-0.5 rounded" /> className="mt-0.5 rounded" />
<div><span className="text-sm">Amendment filed</span><span className="text-xs text-muted-foreground ml-2">An amendment is filed against the bill</span></div> <div className="min-w-0"><span className="text-sm">Amendment filed</span><span className="text-xs text-muted-foreground ml-2">An amendment is filed against the bill</span></div>
</label> </label>
<div className="py-1.5"> <div className="py-1.5">
<label className="flex items-start gap-3 cursor-pointer"> <label className="flex items-start gap-3 cursor-pointer">
<input ref={milestoneCheckRef} type="checkbox" checked={milestoneState === "on"} <input ref={milestoneCheckRef} type="checkbox" checked={milestoneState === "on"}
onChange={toggleMilestones} className="mt-0.5 rounded" /> onChange={toggleMilestones} className="mt-0.5 rounded" />
<div><span className="text-sm font-medium">Milestones</span><span className="text-xs text-muted-foreground ml-2">Select all milestone types</span></div> <div className="min-w-0"><span className="text-sm font-medium">Milestones</span><span className="text-xs text-muted-foreground ml-2">Select all milestone types</span></div>
</label> </label>
<div className="ml-6 mt-1 space-y-0.5"> <div className="ml-6 mt-1 space-y-0.5">
{(["vote", "presidential", "committee_report", "calendar", "procedural"] as const).map((k) => { {(["vote", "presidential", "committee_report", "calendar", "procedural"] as const).map((k) => {
@@ -150,7 +150,7 @@ function ModeFilterSection({
<input type="checkbox" checked={!!filters[k]} <input type="checkbox" checked={!!filters[k]}
onChange={(e) => onChange({ ...filters, [k]: e.target.checked })} onChange={(e) => onChange({ ...filters, [k]: e.target.checked })}
className="mt-0.5 rounded" /> className="mt-0.5 rounded" />
<div><span className="text-sm">{row.label}</span><span className="text-xs text-muted-foreground ml-2">{row.hint}</span></div> <div className="min-w-0"><span className="text-sm">{row.label}</span><span className="text-xs text-muted-foreground ml-2">{row.hint}</span></div>
</label> </label>
); );
})} })}
@@ -484,7 +484,7 @@ export default function NotificationsPage() {
</div> </div>
{/* Channel tab bar */} {/* Channel tab bar */}
<div className="flex gap-0 border-b border-border -mx-6 px-6"> <div className="flex gap-0 border-b border-border -mx-6 px-6 overflow-x-auto">
{([ {([
{ key: "ntfy", label: "ntfy", icon: Bell, active: settings?.ntfy_enabled }, { key: "ntfy", label: "ntfy", icon: Bell, active: settings?.ntfy_enabled },
{ key: "email", label: "Email", icon: Mail, active: settings?.email_enabled }, { key: "email", label: "Email", icon: Mail, active: settings?.email_enabled },
@@ -494,7 +494,7 @@ export default function NotificationsPage() {
<button <button
key={key} key={key}
onClick={() => setActiveChannelTab(key)} onClick={() => setActiveChannelTab(key)}
className={`flex items-center gap-1.5 px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${ className={`shrink-0 flex items-center gap-1.5 px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeChannelTab === key activeChannelTab === key
? "border-primary text-foreground" ? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"
@@ -688,12 +688,12 @@ export default function NotificationsPage() {
</div> </div>
{/* Tab bar */} {/* Tab bar */}
<div className="flex gap-0 border-b border-border"> <div className="flex gap-0 border-b border-border overflow-x-auto">
{MODES.map((mode) => ( {MODES.map((mode) => (
<button <button
key={mode.key} key={mode.key}
onClick={() => setActiveFilterTab(mode.key as ModeKey)} onClick={() => setActiveFilterTab(mode.key as ModeKey)}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${ className={`shrink-0 px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeFilterTab === mode.key activeFilterTab === mode.key
? "border-primary text-foreground" ? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"
@@ -704,7 +704,7 @@ export default function NotificationsPage() {
))} ))}
<button <button
onClick={() => setActiveFilterTab("discovery")} onClick={() => setActiveFilterTab("discovery")}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${ className={`shrink-0 px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeFilterTab === "discovery" activeFilterTab === "discovery"
? "border-primary text-foreground" ? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"
@@ -749,7 +749,7 @@ export default function NotificationsPage() {
<input type="checkbox" checked={!!enabled} <input type="checkbox" checked={!!enabled}
onChange={(e) => setDiscoveryFilters((prev) => ({ ...prev, [src.key]: { ...prev[src.key], enabled: e.target.checked } }))} onChange={(e) => setDiscoveryFilters((prev) => ({ ...prev, [src.key]: { ...prev[src.key], enabled: e.target.checked } }))}
className="rounded" /> className="rounded" />
<div> <div className="min-w-0">
<span className="text-sm font-medium">Notify me about bills from member follows</span> <span className="text-sm font-medium">Notify me about bills from member follows</span>
<span className="text-xs text-muted-foreground ml-2">{src.description}</span> <span className="text-xs text-muted-foreground ml-2">{src.description}</span>
</div> </div>
@@ -807,7 +807,7 @@ export default function NotificationsPage() {
<input type="checkbox" checked={!!enabled} <input type="checkbox" checked={!!enabled}
onChange={(e) => setDiscoveryFilters((prev) => ({ ...prev, [src.key]: { ...prev[src.key], enabled: e.target.checked } }))} onChange={(e) => setDiscoveryFilters((prev) => ({ ...prev, [src.key]: { ...prev[src.key], enabled: e.target.checked } }))}
className="rounded" /> className="rounded" />
<div> <div className="min-w-0">
<span className="text-sm font-medium">Notify me about bills from topic follows</span> <span className="text-sm font-medium">Notify me about bills from topic follows</span>
<span className="text-xs text-muted-foreground ml-2">{src.description}</span> <span className="text-xs text-muted-foreground ml-2">{src.description}</span>
</div> </div>
@@ -1062,7 +1062,7 @@ export default function NotificationsPage() {
<span className="text-xs text-muted-foreground ml-auto">{timeAgo(event.created_at)}</span> <span className="text-xs text-muted-foreground ml-auto">{timeAgo(event.created_at)}</span>
</div> </div>
{billTitle && ( {billTitle && (
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-1">{billTitle}</p> <p className="text-xs text-muted-foreground mt-0.5 line-clamp-1 break-all">{billTitle}</p>
)} )}
{(() => { {(() => {
const src = p.source as string | undefined; const src = p.source as string | undefined;

View File

@@ -42,7 +42,7 @@ export function AuthGuard({ children }: { children: React.ReactNode }) {
// Authenticated or guest browsing: render the full app shell // Authenticated or guest browsing: render the full app shell
return ( return (
<div className="flex h-dvh bg-background"> <div className="fixed inset-0 flex overflow-hidden bg-background">
{/* Desktop sidebar — hidden on mobile */} {/* Desktop sidebar — hidden on mobile */}
<div className="hidden md:flex"> <div className="hidden md:flex">
<Sidebar /> <Sidebar />
@@ -59,7 +59,7 @@ export function AuthGuard({ children }: { children: React.ReactNode }) {
)} )}
{/* Content column */} {/* Content column */}
<div className="flex-1 flex flex-col min-h-0"> <div className="flex-1 flex flex-col min-h-0 min-w-0">
<MobileHeader onMenuClick={() => setDrawerOpen(true)} /> <MobileHeader onMenuClick={() => setDrawerOpen(true)} />
<main className="flex-1 overflow-auto"> <main className="flex-1 overflow-auto">
<div className="container mx-auto px-4 md:px-6 py-6 max-w-7xl"> <div className="container mx-auto px-4 md:px-6 py-6 max-w-7xl">