jtlevine Claude Opus 4.7 (1M context) commited on
Commit
eaf5c00
·
1 Parent(s): 4675133

Mobile responsive: hamburger sidebar + responsive grids

Browse files

Ports the Weather AI 2 sidebar pattern: off-canvas drawer with backdrop
on mobile, persistent rail on lg+. Adds mobile header with menu button.
Replaces inline repeat(N, 1fr) grids with responsive Tailwind classes
across Dashboard, Zones, Disbursements, ProgramDesigner, and Pipeline.
Tour tooltip clamped to viewport width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

frontend/src/App.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import { useCallback, useEffect, useState } from 'react'
2
  import { Routes, Route, useNavigate } from 'react-router-dom'
3
  import Joyride, { type CallBackProps, STATUS, EVENTS, ACTIONS } from 'react-joyride'
 
4
  import Sidebar from './components/Sidebar'
5
  import Dashboard from './pages/Dashboard'
6
  import Zones from './pages/Zones'
@@ -45,6 +46,7 @@ function stepTargetSelector(index: number): string | null {
45
  export default function App() {
46
  const [runTour, setRunTour] = useState(false)
47
  const [stepIndex, setStepIndex] = useState(0)
 
48
  const navigate = useNavigate()
49
 
50
  // Joyride autostart is intentionally disabled in the reskin — the tour
@@ -100,10 +102,23 @@ export default function App() {
100
 
101
  return (
102
  <div className="flex min-h-screen bg-paper">
103
- <Sidebar />
104
 
105
- <main className="flex-1 ml-56">
106
- <div className="px-8 sm:px-12 pt-16 pb-16" style={{ maxWidth: '1180px' }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  <Routes>
108
  <Route path="/" element={<Dashboard />} />
109
  <Route path="/zones" element={<Zones />} />
 
1
  import { useCallback, useEffect, useState } from 'react'
2
  import { Routes, Route, useNavigate } from 'react-router-dom'
3
  import Joyride, { type CallBackProps, STATUS, EVENTS, ACTIONS } from 'react-joyride'
4
+ import { Menu } from 'lucide-react'
5
  import Sidebar from './components/Sidebar'
6
  import Dashboard from './pages/Dashboard'
7
  import Zones from './pages/Zones'
 
46
  export default function App() {
47
  const [runTour, setRunTour] = useState(false)
48
  const [stepIndex, setStepIndex] = useState(0)
49
+ const [sidebarOpen, setSidebarOpen] = useState(false)
50
  const navigate = useNavigate()
51
 
52
  // Joyride autostart is intentionally disabled in the reskin — the tour
 
102
 
103
  return (
104
  <div className="flex min-h-screen bg-paper">
105
+ <Sidebar open={sidebarOpen} onClose={() => setSidebarOpen(false)} />
106
 
107
+ <main className="flex-1 lg:ml-56 min-w-0">
108
+ <header
109
+ className="flex items-center h-12 px-4 lg:hidden"
110
+ style={{ background: '#ffffff' }}
111
+ >
112
+ <button
113
+ onClick={() => setSidebarOpen(true)}
114
+ aria-label="Open navigation menu"
115
+ className="p-2 -ml-2"
116
+ style={{ color: '#606373', background: 'none', border: 'none' }}
117
+ >
118
+ <Menu size={20} />
119
+ </button>
120
+ </header>
121
+ <div className="px-4 sm:px-8 lg:px-12 pt-6 lg:pt-16 pb-16" style={{ maxWidth: '1180px' }}>
122
  <Routes>
123
  <Route path="/" element={<Dashboard />} />
124
  <Route path="/zones" element={<Zones />} />
frontend/src/components/Sidebar.tsx CHANGED
@@ -5,6 +5,7 @@ import {
5
  SlidersHorizontal,
6
  Banknote,
7
  Settings,
 
8
  } from 'lucide-react'
9
 
10
  const NAV_ITEMS = [
@@ -15,15 +16,34 @@ const NAV_ITEMS = [
15
  { to: '/pipeline', label: 'How it works', icon: Settings, tourId: 'nav-pipeline' },
16
  ]
17
 
18
- export default function Sidebar() {
 
 
 
 
 
19
  return (
20
- <aside
21
- className="fixed top-0 left-0 z-50 h-full w-56 flex flex-col"
22
- style={{ background: '#1b1e2d' }}
23
- >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  {/* Brand */}
25
  <div
26
- className="flex items-center h-16 px-5"
27
  style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}
28
  >
29
  <Link to="/" className="flex items-baseline gap-2 no-underline">
@@ -39,6 +59,14 @@ export default function Sidebar() {
39
  Heat insurance
40
  </span>
41
  </Link>
 
 
 
 
 
 
 
 
42
  </div>
43
 
44
  {/* Navigation */}
@@ -49,6 +77,7 @@ export default function Sidebar() {
49
  to={to}
50
  end={to === '/'}
51
  data-tour={tourId}
 
52
  className="sidebar-link"
53
  >
54
  <Icon size={16} style={{ flexShrink: 0 }} />
@@ -71,6 +100,8 @@ export default function Sidebar() {
71
  Worker personas are simulated
72
  </div>
73
 
 
 
74
  <style>{`
75
  .sidebar-link {
76
  display: flex;
@@ -93,6 +124,6 @@ export default function Sidebar() {
93
  border-left-color: #8a3d28;
94
  }
95
  `}</style>
96
- </aside>
97
  )
98
  }
 
5
  SlidersHorizontal,
6
  Banknote,
7
  Settings,
8
+ X,
9
  } from 'lucide-react'
10
 
11
  const NAV_ITEMS = [
 
16
  { to: '/pipeline', label: 'How it works', icon: Settings, tourId: 'nav-pipeline' },
17
  ]
18
 
19
+ interface Props {
20
+ open: boolean
21
+ onClose: () => void
22
+ }
23
+
24
+ export default function Sidebar({ open, onClose }: Props) {
25
  return (
26
+ <>
27
+ {open && (
28
+ <div
29
+ className="fixed inset-0 z-40 lg:hidden"
30
+ style={{ background: 'rgba(27, 30, 45, 0.4)' }}
31
+ onClick={onClose}
32
+ />
33
+ )}
34
+
35
+ <aside
36
+ className={`
37
+ fixed top-0 left-0 z-50 h-full w-56
38
+ flex flex-col transition-transform duration-200 ease-in-out
39
+ lg:translate-x-0
40
+ ${open ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
41
+ `}
42
+ style={{ background: '#1b1e2d' }}
43
+ >
44
  {/* Brand */}
45
  <div
46
+ className="flex items-center justify-between h-16 px-5"
47
  style={{ borderBottom: '1px solid rgba(255,255,255,0.06)' }}
48
  >
49
  <Link to="/" className="flex items-baseline gap-2 no-underline">
 
59
  Heat insurance
60
  </span>
61
  </Link>
62
+ <button
63
+ onClick={onClose}
64
+ aria-label="Close navigation menu"
65
+ className="lg:hidden p-1"
66
+ style={{ color: '#8d909e', background: 'none', border: 'none' }}
67
+ >
68
+ <X size={18} />
69
+ </button>
70
  </div>
71
 
72
  {/* Navigation */}
 
77
  to={to}
78
  end={to === '/'}
79
  data-tour={tourId}
80
+ onClick={onClose}
81
  className="sidebar-link"
82
  >
83
  <Icon size={16} style={{ flexShrink: 0 }} />
 
100
  Worker personas are simulated
101
  </div>
102
 
103
+ </aside>
104
+
105
  <style>{`
106
  .sidebar-link {
107
  display: flex;
 
124
  border-left-color: #8a3d28;
125
  }
126
  `}</style>
127
+ </>
128
  )
129
  }
frontend/src/lib/tour.ts CHANGED
@@ -135,7 +135,7 @@ export const tourStyles = {
135
  tooltip: {
136
  borderRadius: 10,
137
  padding: '20px 22px',
138
- maxWidth: 400,
139
  fontFamily: '"DM Sans", system-ui, sans-serif',
140
  fontSize: '0.88rem',
141
  lineHeight: 1.6,
 
135
  tooltip: {
136
  borderRadius: 10,
137
  padding: '20px 22px',
138
+ maxWidth: 'min(400px, calc(100vw - 32px))',
139
  fontFamily: '"DM Sans", system-ui, sans-serif',
140
  fontSize: '0.88rem',
141
  lineHeight: 1.6,
frontend/src/pages/Dashboard.tsx CHANGED
@@ -282,16 +282,12 @@ function PipelineHero() {
282
 
283
  {/* Step row */}
284
  <div
285
- style={{
286
- display: 'grid',
287
- gridTemplateColumns: `repeat(${HERO_STEPS.length}, 1fr)`,
288
- gap: '8px',
289
- position: 'relative',
290
- }}
291
  data-tour="stage-cards"
292
  >
293
  {/* connector line animated on mount */}
294
  <div
 
295
  style={{
296
  position: 'absolute',
297
  top: '20px',
@@ -311,6 +307,7 @@ function PipelineHero() {
311
  <button
312
  key={s.num}
313
  onMouseEnter={() => !locked && setSelected(i)}
 
314
  onClick={() => {
315
  setSelected(i)
316
  setLocked(true)
@@ -393,13 +390,11 @@ function PipelineHero() {
393
  {/* Detail + output panel */}
394
  <div
395
  key={step.num}
396
- className="animate-fade-in"
397
  style={{
398
  marginTop: '24px',
399
  paddingTop: '20px',
400
  borderTop: '1px solid #e8e5e1',
401
- display: 'grid',
402
- gridTemplateColumns: 'minmax(0, 1fr) minmax(0, 1.1fr)',
403
  columnGap: '32px',
404
  rowGap: '14px',
405
  alignItems: 'start',
@@ -502,10 +497,8 @@ export default function Dashboard() {
502
  {/* KPI row */}
503
  <div
504
  data-tour="metrics"
 
505
  style={{
506
- display: 'grid',
507
- gridTemplateColumns: 'repeat(4, 1fr)',
508
- gap: '32px',
509
  borderTop: '1px solid #e8e5e1',
510
  paddingTop: '20px',
511
  marginTop: '12px',
 
282
 
283
  {/* Step row */}
284
  <div
285
+ className="grid grid-cols-2 md:grid-cols-4 gap-3 md:gap-2 relative"
 
 
 
 
 
286
  data-tour="stage-cards"
287
  >
288
  {/* connector line animated on mount */}
289
  <div
290
+ className="hidden md:block"
291
  style={{
292
  position: 'absolute',
293
  top: '20px',
 
307
  <button
308
  key={s.num}
309
  onMouseEnter={() => !locked && setSelected(i)}
310
+ onFocus={() => !locked && setSelected(i)}
311
  onClick={() => {
312
  setSelected(i)
313
  setLocked(true)
 
390
  {/* Detail + output panel */}
391
  <div
392
  key={step.num}
393
+ className="animate-fade-in grid grid-cols-1 md:[grid-template-columns:minmax(0,1fr)_minmax(0,1.1fr)]"
394
  style={{
395
  marginTop: '24px',
396
  paddingTop: '20px',
397
  borderTop: '1px solid #e8e5e1',
 
 
398
  columnGap: '32px',
399
  rowGap: '14px',
400
  alignItems: 'start',
 
497
  {/* KPI row */}
498
  <div
499
  data-tour="metrics"
500
+ className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8"
501
  style={{
 
 
 
502
  borderTop: '1px solid #e8e5e1',
503
  paddingTop: '20px',
504
  marginTop: '12px',
frontend/src/pages/Disbursements.tsx CHANGED
@@ -322,7 +322,8 @@ function SimulatedPayoutTab({
322
  border: '1px solid #e8e5e1',
323
  background: '#fcfaf7',
324
  borderRadius: '2px',
325
- minWidth: '220px',
 
326
  }}
327
  >
328
  {zonesWithEnrollees.map((z) => {
@@ -879,10 +880,8 @@ export default function Disbursements() {
879
  {/* Metric row */}
880
  <div
881
  data-tour="disbursements-metrics"
 
882
  style={{
883
- display: 'grid',
884
- gridTemplateColumns: 'repeat(4, 1fr)',
885
- gap: '32px',
886
  borderTop: '1px solid #e8e5e1',
887
  paddingTop: '28px',
888
  marginBottom: '40px',
 
322
  border: '1px solid #e8e5e1',
323
  background: '#fcfaf7',
324
  borderRadius: '2px',
325
+ width: '100%',
326
+ maxWidth: '320px',
327
  }}
328
  >
329
  {zonesWithEnrollees.map((z) => {
 
880
  {/* Metric row */}
881
  <div
882
  data-tour="disbursements-metrics"
883
+ className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8"
884
  style={{
 
 
 
885
  borderTop: '1px solid #e8e5e1',
886
  paddingTop: '28px',
887
  marginBottom: '40px',
frontend/src/pages/Pipeline.tsx CHANGED
@@ -44,10 +44,8 @@ function HowItWorksSection() {
44
  return (
45
  <div style={{ marginBottom: '24px' }}>
46
  <div
 
47
  style={{
48
- display: 'grid',
49
- gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
50
- gap: '28px',
51
  borderTop: '1px solid #e8e5e1',
52
  paddingTop: '18px',
53
  }}
@@ -109,11 +107,7 @@ function ScalingCostPanel() {
109
  What it costs to run at scale
110
  </div>
111
  <div
112
- style={{
113
- display: 'grid',
114
- gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
115
- gap: '32px',
116
- }}
117
  >
118
  {TIERS.map((tier) => (
119
  <div key={tier.label} style={{ borderTop: '1px solid #e8e5e1', paddingTop: '14px' }}>
@@ -550,11 +544,7 @@ export default function Pipeline() {
550
  Processing steps
551
  </div>
552
  <div
553
- style={{
554
- display: 'grid',
555
- gridTemplateColumns: 'repeat(6, minmax(0, 1fr))',
556
- gap: '16px',
557
- }}
558
  >
559
  {run.steps.map((step, i) => (
560
  <div
 
44
  return (
45
  <div style={{ marginBottom: '24px' }}>
46
  <div
47
+ className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-7"
48
  style={{
 
 
 
49
  borderTop: '1px solid #e8e5e1',
50
  paddingTop: '18px',
51
  }}
 
107
  What it costs to run at scale
108
  </div>
109
  <div
110
+ className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8"
 
 
 
 
111
  >
112
  {TIERS.map((tier) => (
113
  <div key={tier.label} style={{ borderTop: '1px solid #e8e5e1', paddingTop: '14px' }}>
 
544
  Processing steps
545
  </div>
546
  <div
547
+ className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4"
 
 
 
 
548
  >
549
  {run.steps.map((step, i) => (
550
  <div
frontend/src/pages/ProgramDesigner.tsx CHANGED
@@ -132,10 +132,8 @@ export default function ProgramDesigner() {
132
  <div data-tour="program-scope" style={{ marginBottom: '48px' }}>
133
  <div className="section-header">Who's covered</div>
134
  <div
 
135
  style={{
136
- display: 'grid',
137
- gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
138
- gap: '32px',
139
  borderTop: '1px solid #e8e5e1',
140
  paddingTop: '24px',
141
  }}
@@ -167,12 +165,10 @@ export default function ProgramDesigner() {
167
  <div data-tour="program-cost" style={{ marginBottom: '48px' }}>
168
  <div className="section-header">What it costs</div>
169
  <div
 
170
  style={{
171
  borderTop: '1px solid #e8e5e1',
172
  paddingTop: '28px',
173
- display: 'grid',
174
- gridTemplateColumns: 'minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr)',
175
- gap: '32px',
176
  alignItems: 'end',
177
  }}
178
  >
 
132
  <div data-tour="program-scope" style={{ marginBottom: '48px' }}>
133
  <div className="section-header">Who's covered</div>
134
  <div
135
+ className="grid grid-cols-1 sm:grid-cols-2 gap-6 sm:gap-8"
136
  style={{
 
 
 
137
  borderTop: '1px solid #e8e5e1',
138
  paddingTop: '24px',
139
  }}
 
165
  <div data-tour="program-cost" style={{ marginBottom: '48px' }}>
166
  <div className="section-header">What it costs</div>
167
  <div
168
+ className="grid grid-cols-1 md:grid-cols-[1.4fr_1fr_1fr] gap-6 md:gap-8"
169
  style={{
170
  borderTop: '1px solid #e8e5e1',
171
  paddingTop: '28px',
 
 
 
172
  alignItems: 'end',
173
  }}
174
  >
frontend/src/pages/Zones.tsx CHANGED
@@ -97,10 +97,8 @@ export default function Zones() {
97
  {/* Metric row */}
98
  <div
99
  data-tour="zones-metrics"
 
100
  style={{
101
- display: 'grid',
102
- gridTemplateColumns: 'repeat(4, 1fr)',
103
- gap: '32px',
104
  borderTop: '1px solid #e8e5e1',
105
  paddingTop: '28px',
106
  marginBottom: '36px',
 
97
  {/* Metric row */}
98
  <div
99
  data-tour="zones-metrics"
100
+ className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8"
101
  style={{
 
 
 
102
  borderTop: '1px solid #e8e5e1',
103
  paddingTop: '28px',
104
  marginBottom: '36px',