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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user