Make app fully responsive for smartphones

Layout:
- Remove body min-width: 1280px
- Sidebar hidden on mobile (<768px); replaced by a fixed bottom navigation bar (BottomNav component) with icon + label for all 7 routes
- PageShell main gets className="app-main"; padding overridden to 0 on mobile (68px bottom clearance for bottom nav)
- All page root divs get className="page-body"; mobile override to 12px 14px padding

Dashboard:
- 4-col stat grid → 2×2 on mobile (dash-stat-grid)
- 3-col timing/targets/forecast → single column (dash-three-col)
- 2-col monthly/best-nights → single column (dash-two-col)
- Run order timeline: overflow-x: auto for narrow screens

Targets:
- Filter bar rows get className="filter-row": horizontal scroll on mobile (no-wrap + overflow-x: auto)
- Table gets className="targets-table": columns 3-7 (Size/Fill/Mosaic/Mag/Diff) and 10+ (Vis/Int/Goal/Compare) hidden on mobile, keeping only Type, Name, Filter, Alt

Calendar:
- Split view gets dash-two-col (stacks on mobile)
- Month grid: minWidth 308px + overflowX auto; cell minWidth 44px for touch targets

Compare modal: columns stack vertically on mobile (compare-body / compare-col classes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 07:35:10 +02:00
parent 2bb80a8475
commit 561de4f13b
11 changed files with 141 additions and 84 deletions
+3 -2
View File
@@ -1,5 +1,5 @@
import type { ReactNode } from 'react';
import Sidebar from './Sidebar';
import Sidebar, { BottomNav } from './Sidebar';
interface Props {
children: ReactNode;
@@ -9,7 +9,7 @@ export default function PageShell({ children }: Props) {
return (
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
<Sidebar />
<main style={{
<main className="app-main" style={{
flex: 1,
overflow: 'auto',
background: 'var(--bg-void)',
@@ -17,6 +17,7 @@ export default function PageShell({ children }: Props) {
}}>
{children}
</main>
<BottomNav />
</div>
);
}
+36 -60
View File
@@ -15,29 +15,43 @@ const TRANSP_LABELS: Record<number, string> = {
const navItems = [
{ path: '/dashboard', label: 'Dashboard', icon: '⬡' },
{ path: '/targets', label: 'Targets', icon: '✦' },
{ path: '/calendar', label: 'Calendar', icon: '◫' },
{ path: '/stats', label: 'Statistics', icon: '▤' },
{ path: '/gallery', label: 'Gallery', icon: '⬚' },
{ path: '/solar-system', label: 'Solar System', icon: '◉' },
{ path: '/settings', label: 'Settings', icon: '⚙' },
{ path: '/targets', label: 'Targets', icon: '✦' },
{ path: '/calendar', label: 'Calendar', icon: '◫' },
{ path: '/stats', label: 'Stats', icon: '▤' },
{ path: '/gallery', label: 'Gallery', icon: '⬚' },
{ path: '/solar-system', label: 'Solar', icon: '◉' },
{ path: '/settings', label: 'Settings', icon: '⚙' },
];
function fmtTime(utc?: string): string {
if (!utc) return '—';
return new Date(utc).toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/Paris',
hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris',
});
}
export function BottomNav() {
return (
<nav className="bottom-nav">
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) => isActive ? 'active' : ''}
>
<span className="bnav-icon">{item.icon}</span>
{item.label}
</NavLink>
))}
</nav>
);
}
export default function Sidebar() {
const { data: tonight } = useTonight();
const { data: weather } = useWeather();
const { data: forecast } = useForecast();
// First forecast slot = current/nearest 3-hour window
const slot = (forecast as { dataseries?: { seeing?: number; transparency?: number; cloudcover?: number }[] })?.dataseries?.[0];
const darkStart = tonight?.true_dark_start_utc;
@@ -50,13 +64,8 @@ export default function Sidebar() {
? (weather.temp_c - weather.dew_point_c).toFixed(1)
: null;
const seeingMap: Record<number, string> = {
1: '0.5″', 2: '0.75″', 3: '1.0″', 4: '1.25″',
5: '1.5″', 6: '2.0″', 7: '2.5″', 8: '>3″',
};
return (
<aside style={{
<aside className="app-sidebar" style={{
width: 220,
minWidth: 220,
background: 'var(--bg-deep)',
@@ -69,14 +78,10 @@ export default function Sidebar() {
overflow: 'hidden',
}}>
{/* Logo */}
<div style={{
padding: '20px 20px 16px',
borderBottom: '1px solid var(--border)',
}}>
<div style={{ padding: '20px 20px 16px', borderBottom: '1px solid var(--border)' }}>
<div style={{
fontFamily: 'var(--font-display)',
fontSize: 16,
fontWeight: 700,
fontSize: 16, fontWeight: 700,
color: 'var(--amber)',
letterSpacing: '0.06em',
textTransform: 'uppercase',
@@ -92,16 +97,13 @@ export default function Sidebar() {
key={item.path}
to={item.path}
style={({ isActive }) => ({
display: 'flex',
alignItems: 'center',
gap: 10,
display: 'flex', alignItems: 'center', gap: 10,
padding: '9px 20px',
color: isActive ? 'var(--text-hi)' : 'var(--text-mid)',
background: isActive ? 'var(--bg-hover)' : 'transparent',
borderLeft: `2px solid ${isActive ? 'var(--amber)' : 'transparent'}`,
fontFamily: 'var(--font-display)',
fontSize: 13,
fontWeight: isActive ? 700 : 400,
fontSize: 13, fontWeight: isActive ? 700 : 400,
letterSpacing: '0.05em',
textDecoration: 'none',
transition: 'color 0.1s',
@@ -114,18 +116,8 @@ export default function Sidebar() {
</nav>
{/* Tonight widget */}
<div style={{
borderTop: '1px solid var(--border)',
padding: '12px 16px',
}}>
<div style={{
fontSize: 10,
fontFamily: 'var(--font-mono)',
color: 'var(--text-lo)',
letterSpacing: '0.1em',
marginBottom: 8,
textTransform: 'uppercase',
}}>
<div style={{ borderTop: '1px solid var(--border)', padding: '12px 16px' }}>
<div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-lo)', letterSpacing: '0.1em', marginBottom: 8, textTransform: 'uppercase' }}>
Tonight
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
@@ -144,9 +136,7 @@ export default function Sidebar() {
<td style={{ color: 'var(--text-lo)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>Moon</td>
<td style={{ textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 4 }}>
<span style={{ color: 'var(--text-mid)', fontSize: 11, fontFamily: 'var(--font-mono)' }}>
{tonight?.moon_illumination != null
? `${Math.round(tonight.moon_illumination * 100)}%`
: '—'}
{tonight?.moon_illumination != null ? `${Math.round(tonight.moon_illumination * 100)}%` : '—'}
</span>
{tonight?.moon_illumination != null && (
<MoonPhaseIcon illumination={tonight.moon_illumination} size={14} />
@@ -158,18 +148,8 @@ export default function Sidebar() {
</div>
{/* Conditions widget */}
<div style={{
borderTop: '1px solid var(--border)',
padding: '12px 16px',
}}>
<div style={{
fontSize: 10,
fontFamily: 'var(--font-mono)',
color: 'var(--text-lo)',
letterSpacing: '0.1em',
marginBottom: 8,
textTransform: 'uppercase',
}}>
<div style={{ borderTop: '1px solid var(--border)', padding: '12px 16px' }}>
<div style={{ fontSize: 10, fontFamily: 'var(--font-mono)', color: 'var(--text-lo)', letterSpacing: '0.1em', marginBottom: 8, textTransform: 'uppercase' }}>
Conditions
</div>
<div style={{ marginBottom: 6 }}>
@@ -186,12 +166,8 @@ export default function Sidebar() {
<tr key={label}>
<td style={{ color: 'var(--text-lo)', fontSize: 11, fontFamily: 'var(--font-mono)', paddingBottom: 3 }}>{label}</td>
<td style={{
color: label === 'Dew Δ' && dewMargin && parseFloat(dewMargin) < 4
? 'var(--danger)'
: 'var(--text-mid)',
fontSize: 11,
fontFamily: 'var(--font-mono)',
textAlign: 'right',
color: label === 'Dew Δ' && dewMargin && parseFloat(dewMargin) < 4 ? 'var(--danger)' : 'var(--text-mid)',
fontSize: 11, fontFamily: 'var(--font-mono)', textAlign: 'right',
}}>{value as string}</td>
</tr>
))}
@@ -230,7 +230,7 @@ export default function CompareModal({ targets, onClose }: Props) {
>
<div style={{
background: 'var(--bg-panel)', border: '1px solid var(--border-hi)',
borderRadius: 8, width: '100%', maxWidth: 1100, maxHeight: '90vh',
borderRadius: 8, width: '100%', maxWidth: 1100, maxHeight: '95vh',
display: 'flex', flexDirection: 'column', overflow: 'hidden',
}}>
{/* Header */}
@@ -250,11 +250,11 @@ export default function CompareModal({ targets, onClose }: Props) {
</div>
{/* Body */}
<div style={{ display: 'flex', gap: 0, overflow: 'auto', flex: 1 }}>
<div style={{ flex: 1, padding: 20, overflow: 'auto', borderRight: '1px solid var(--border)' }}>
<div className="compare-body" style={{ display: 'flex', gap: 0, overflow: 'auto', flex: 1 }}>
<div className="compare-col" style={{ flex: 1, padding: 20, overflow: 'auto', borderRight: '1px solid var(--border)' }}>
<TargetColumn target={targets[0]} />
</div>
<div style={{ flex: 1, padding: 20, overflow: 'auto' }}>
<div className="compare-col" style={{ flex: 1, padding: 20, overflow: 'auto' }}>
<TargetColumn target={targets[1]} />
</div>
</div>