Skip to content

Commit cd9a552

Browse files
fix: centralize iOS safe area and refactor list components (#47)
* fix: add iOS safe-area-inset-top support for fullscreen overlays - Add pt-safe CSS utility class for safe-area-inset-top - Create FullscreenSheet component with built-in safe-area support - Add fullscreen prop to DialogContent for fullscreen dialogs - Update FileBrowserSheet to use FullscreenSheet - Update MobileFilePreviewModal to use FullscreenSheet - Add pt-safe to SettingsDialog mobile view Fixes status bar overlap on iOS devices with notch/Dynamic Island * fix: add pt-safe to main page headers - Header.tsx (main repos page) - SessionDetailHeader.tsx (session page) * docs: add TDD refactoring plan for iOS safe area * remove plan file from repo * refactor: centralize iOS safe area handling with PageHeader component - Add PageHeader component with pt-safe, sticky, z-10, border-b styling - Add TDD test infrastructure (vitest config, testing-library, jsdom) - Refactor Header, SessionDetailHeader, RepoDetailHeader, Workspace to use PageHeader - Fix missing pt-safe on RepoDetailHeader and Workspace pages - All header safe area styling now has single fix point * fix: add pt-safe to remaining fullscreen dialogs and sheets - GitChangesSheet, FilePreviewDialog: add fullscreen prop - ModelSelectDialog, RepoMcpDialog: add pt-safe class - EditSessionTitleDialog: add pt-safe on mobile * fix: use mobile-first positioning for large dialogs Dialogs need top-0 positioning on mobile for pt-safe to work. Centered dialogs don't benefit from safe area padding. * fix: add mobileFullscreen prop to DialogContent - Fullscreen on mobile with pt-safe, centered on desktop - Simplifies large dialog styling * fix: use inline styles for iOS safe area to fix CSS cascade issues - Convert pt-safe/pb-safe to @Utility directives with iOS 11 fallback - Use inline styles on DialogContent for reliable safe-area padding - Position close button with calc(env(safe-area-inset-top) + 1rem) - Add 10 tests for DialogContent safe-area behavior * refactor: extract ListToolbar and Header components for reuse - Add ListToolbar component for search/select all/delete UI - Add Header compound component (Header.Root, Header.Title, etc.) - Update SessionList and RepoList to use ListToolbar - Mobile delete: deletes selected or all if none selected - Cancel delete now clears selection - Remove unused layout components --------- Co-authored-by: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com>
1 parent 72bb20a commit cd9a552

34 files changed

Lines changed: 1627 additions & 986 deletions

frontend/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "vite",
77
"build": "tsc -b && vite build",
88
"lint": "eslint .",
9-
"preview": "vite preview"
9+
"preview": "vite preview",
10+
"test": "vitest"
1011
},
1112
"dependencies": {
1213
"@hookform/resolvers": "^5.2.2",
@@ -48,6 +49,9 @@
4849
},
4950
"devDependencies": {
5051
"@eslint/js": "^9.36.0",
52+
"@testing-library/jest-dom": "^6.9.1",
53+
"@testing-library/react": "^16.3.1",
54+
"@testing-library/user-event": "^14.6.1",
5155
"@types/node": "^24.8.1",
5256
"@types/react": "^19.2.2",
5357
"@types/react-dom": "^19.2.2",
@@ -57,6 +61,7 @@
5761
"eslint-plugin-react-hooks": "^5.2.0",
5862
"eslint-plugin-react-refresh": "^0.4.22",
5963
"globals": "^16.4.0",
64+
"jsdom": "^27.4.0",
6065
"openapi-typescript": "^7.10.1",
6166
"postcss": "^8.5.6",
6267
"tailwindcss": "^4.1.14",

frontend/src/App.css

Lines changed: 0 additions & 42 deletions
This file was deleted.

frontend/src/components/file-browser/FileBrowserSheet.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useEffect, useState, memo, useCallback, useRef } from 'react'
22
import { FileBrowser } from './FileBrowser'
33
import { Button } from '@/components/ui/button'
44
import { PathDisplay } from '@/components/ui/path-display'
5+
import { FullscreenSheet, FullscreenSheetHeader, FullscreenSheetContent } from '@/components/ui/fullscreen-sheet'
56
import { X } from 'lucide-react'
67
import { GPU_ACCELERATED_STYLE, MODAL_TRANSITION_MS } from '@/lib/utils'
78
import { useSwipeBack } from '@/hooks/useMobile'
@@ -37,6 +38,7 @@ export const FileBrowserSheet = memo(function FileBrowserSheet({ isOpen, onClose
3738
return () => clearTimeout(timer)
3839
}
3940
}, [isOpen])
41+
4042
const handleDirectoryLoad = useCallback((info: { workspaceRoot?: string; currentPath: string }) => {
4143
if (!info.currentPath || info.currentPath === '.' || info.currentPath === '') {
4244
setDisplayPath('/')
@@ -94,11 +96,8 @@ export const FileBrowserSheet = memo(function FileBrowserSheet({ isOpen, onClose
9496
transition: 'opacity 150ms ease-out',
9597
}}
9698
>
97-
<div
98-
className="absolute inset-0 bg-background flex flex-col"
99-
style={{ ...GPU_ACCELERATED_STYLE, ...swipeStyles }}
100-
>
101-
<div className="flex-shrink-0 border-b border-border bg-background backdrop-blur-sm px-4 py-1">
99+
<FullscreenSheet style={{ ...GPU_ACCELERATED_STYLE, ...swipeStyles }}>
100+
<FullscreenSheetHeader className="px-4 py-1">
102101
<div className="flex items-center justify-between">
103102
<div className="flex items-center gap-3">
104103
{(displayPath === '/' || !repoName) && (
@@ -119,17 +118,17 @@ export const FileBrowserSheet = memo(function FileBrowserSheet({ isOpen, onClose
119118
</Button>
120119
)}
121120
</div>
122-
</div>
121+
</FullscreenSheetHeader>
123122

124-
<div className="flex-1 overflow-hidden min-h-0">
123+
<FullscreenSheetContent>
125124
<FileBrowser
126125
basePath={normalizedBasePath}
127126
embedded={true}
128127
initialSelectedFile={initialSelectedFile}
129128
onDirectoryLoad={handleDirectoryLoad}
130129
/>
131-
</div>
132-
</div>
130+
</FullscreenSheetContent>
131+
</FullscreenSheet>
133132
</div>
134133
)
135134
})

frontend/src/components/file-browser/FilePreviewDialog.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function FilePreviewDialog({ isOpen, onClose, filePath, repoBasePath, onF
6161
<DialogContent
6262
className="w-screen h-screen max-w-none max-h-none p-0 bg-background border-0 flex flex-col"
6363
hideCloseButton
64+
fullscreen
6465
>
6566
<div className="flex-1 overflow-hidden min-h-0">
6667
{isLoading ? (

frontend/src/components/file-browser/GitChangesSheet.tsx

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { useState, useEffect, useRef } from 'react'
2-
import { Dialog, DialogContent } from '@/components/ui/dialog'
2+
import { createPortal } from 'react-dom'
33
import { GitChangesPanel } from './GitChangesPanel'
44
import { FileDiffView } from './FileDiffView'
55
import { FilePreviewDialog } from './FilePreviewDialog'
6+
import { FullscreenSheet, FullscreenSheetHeader, FullscreenSheetContent } from '@/components/ui/fullscreen-sheet'
67
import { Button } from '@/components/ui/button'
78
import { X, GitBranch } from 'lucide-react'
89
import { useMobile, useSwipeBack } from '@/hooks/useMobile'
910
import { useQueryClient } from '@tanstack/react-query'
11+
import { GPU_ACCELERATED_STYLE, MODAL_TRANSITION_MS } from '@/lib/utils'
1012

1113
interface GitChangesSheetProps {
1214
isOpen: boolean
@@ -63,39 +65,66 @@ export function GitChangesSheet({ isOpen, onClose, repoId, currentBranch, repoLo
6365
queryClient.invalidateQueries({ queryKey: ['fileDiff'] })
6466
}
6567

66-
const handleOpenChange = (open: boolean) => {
67-
if (!open) {
68-
onClose()
68+
const [shouldRender, setShouldRender] = useState(false)
69+
70+
useEffect(() => {
71+
if (isOpen) {
72+
setShouldRender(true)
73+
document.body.style.overflow = 'hidden'
74+
} else {
75+
const timer = setTimeout(() => setShouldRender(false), MODAL_TRANSITION_MS)
76+
document.body.style.overflow = 'unset'
77+
return () => clearTimeout(timer)
6978
}
70-
}
79+
return () => {
80+
document.body.style.overflow = 'unset'
81+
}
82+
}, [isOpen])
7183

72-
return (
73-
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
74-
<DialogContent
75-
ref={contentRef}
76-
className="w-screen h-screen max-w-none max-h-none p-0 gap-0 bg-background border-0 flex flex-col"
77-
hideCloseButton
78-
style={swipeStyles}
79-
>
80-
<div className="flex items-center justify-between px-4 sm:py-3 border-b border-border flex-shrink-0">
81-
<div className="flex items-center gap-2">
82-
<GitBranch className="w-4 h-4 text-foreground" />
83-
<h2 className="text-sm font-semibold text-foreground">
84-
{selectedFile ? 'File Changes' : 'Git Changes'}
85-
</h2>
86-
<span className="text-xs text-muted-foreground">({currentBranch})</span>
84+
useEffect(() => {
85+
const handleEscape = (e: KeyboardEvent) => {
86+
if (e.key === 'Escape' && isOpen) {
87+
onClose()
88+
}
89+
}
90+
document.addEventListener('keydown', handleEscape)
91+
return () => document.removeEventListener('keydown', handleEscape)
92+
}, [isOpen, onClose])
93+
94+
if (!isOpen && !shouldRender) return null
95+
96+
return createPortal(
97+
<div
98+
ref={contentRef}
99+
className="fixed inset-0 z-50"
100+
style={{
101+
opacity: isOpen ? 1 : 0,
102+
pointerEvents: isOpen ? 'auto' : 'none',
103+
transition: 'opacity 150ms ease-out',
104+
}}
105+
>
106+
<FullscreenSheet style={{ ...GPU_ACCELERATED_STYLE, ...swipeStyles }}>
107+
<FullscreenSheetHeader className="px-4 py-1">
108+
<div className="flex items-center justify-between">
109+
<div className="flex items-center gap-2">
110+
<GitBranch className="w-4 h-4 text-foreground" />
111+
<h2 className="text-sm font-semibold text-foreground">
112+
{selectedFile ? 'File Changes' : 'Git Changes'}
113+
</h2>
114+
<span className="text-xs text-muted-foreground">({currentBranch})</span>
115+
</div>
116+
<Button
117+
variant="ghost"
118+
size="icon"
119+
onClick={onClose}
120+
className="text-muted-foreground hover:text-foreground hover:bg-muted transition-all duration-200 h-8 w-8"
121+
>
122+
<X className="w-4 h-4" />
123+
</Button>
87124
</div>
88-
<Button
89-
variant="ghost"
90-
size="icon"
91-
onClick={onClose}
92-
className="text-muted-foreground hover:text-foreground hover:bg-muted transition-all duration-200 h-8 w-8"
93-
>
94-
<X className="w-4 h-4" />
95-
</Button>
96-
</div>
125+
</FullscreenSheetHeader>
97126

98-
<div className="flex-1 overflow-hidden min-h-0">
127+
<FullscreenSheetContent>
99128
{isMobile ? (
100129
selectedFile ? (
101130
<FileDiffView
@@ -137,7 +166,7 @@ export function GitChangesSheet({ isOpen, onClose, repoId, currentBranch, repoLo
137166
</div>
138167
</div>
139168
)}
140-
</div>
169+
</FullscreenSheetContent>
141170

142171
<FilePreviewDialog
143172
isOpen={!!previewFilePath}
@@ -147,7 +176,8 @@ export function GitChangesSheet({ isOpen, onClose, repoId, currentBranch, repoLo
147176
onFileSaved={handleFileSaved}
148177
initialLineNumber={previewLineNumber}
149178
/>
150-
</DialogContent>
151-
</Dialog>
179+
</FullscreenSheet>
180+
</div>,
181+
document.body
152182
)
153183
}

frontend/src/components/file-browser/MobileFilePreviewModal.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { memo, useCallback, useState, useEffect, useRef } from "react";
22
import { FilePreview } from "./FilePreview";
3+
import { FullscreenSheet } from "@/components/ui/fullscreen-sheet";
34
import type { FileInfo } from "@/types/files";
45
import { GPU_ACCELERATED_STYLE, MODAL_TRANSITION_MS } from "@/lib/utils";
56
import { useSwipeBack } from "@/hooks/useMobile";
@@ -51,21 +52,17 @@ export const MobileFilePreviewModal = memo(function MobileFilePreviewModal({
5152
}
5253

5354
return (
54-
<div
55-
ref={containerRef}
56-
className="fixed inset-0 z-50 bg-background"
57-
style={{ isolation: 'isolate', ...GPU_ACCELERATED_STYLE, ...swipeStyles }}
58-
>
59-
<div
60-
className={`h-full overflow-hidden bg-background ${showFilePreviewHeader ? "" : "pb-8"}`}
61-
>
62-
<FilePreview
63-
file={localFile}
64-
hideHeader={!showFilePreviewHeader}
65-
isMobileModal={showFilePreviewHeader}
66-
onCloseModal={handleClose}
67-
/>
68-
</div>
55+
<div ref={containerRef} style={{ isolation: 'isolate' }}>
56+
<FullscreenSheet style={{ ...GPU_ACCELERATED_STYLE, ...swipeStyles }}>
57+
<div className={`h-full overflow-hidden bg-background pt-safe ${showFilePreviewHeader ? "" : "pb-8"}`}>
58+
<FilePreview
59+
file={localFile}
60+
hideHeader={!showFilePreviewHeader}
61+
isMobileModal={showFilePreviewHeader}
62+
onCloseModal={handleClose}
63+
/>
64+
</div>
65+
</FullscreenSheet>
6966
</div>
7067
);
7168
})

frontend/src/components/layout/Header.tsx

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)