Skip to content

Commit 0700c3c

Browse files
committed
feat: full mobile support with swipe nav, touch gestures, and responsive layout
- Add swipe navigation for workspace switching on mobile - Enable Swapy drag-and-drop on touch devices - Add mobile bottom nav with workspace numbers + pagination dots - Add compact mobile top bar (clock, label, search/theme/help icons) - Add responsive slide-up overlays for rofi, keybinds, theme picker - Close button (X) on floating windows visible only on mobile - Active window focus deactivates transparency on mobile - Mobile help shows touch gestures instead of desktop keybinds - Safe area variables, touch-friendly min sizes, table stacking - Email link with underline on About, remove redundant site row
1 parent afd0488 commit 0700c3c

12 files changed

Lines changed: 583 additions & 172 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ dist
1212
dist-ssr
1313
*.local
1414

15+
# OpenCode / Gentle AI artifacts
16+
.atl/
17+
1518
# Editor directories and files
1619
.vscode/*
1720
!.vscode/extensions.json

index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
6+
<meta name="apple-mobile-web-app-capable" content="yes" />
7+
<meta name="mobile-web-app-capable" content="yes" />
68

79
<!-- Favicon -->
810
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function AppContent() {
2727
}, [dispatch])
2828

2929
return (
30-
<div className="flex flex-col min-h-svh pt-12 bg-[var(--color-bg-base)] relative">
30+
<div className="flex flex-col min-h-svh pt-12 max-md:pt-0 max-md:pb-[56px] bg-[var(--color-bg-base)] relative">
3131
<div
3232
aria-hidden="true"
3333
className="fixed inset-0 z-0 bg-cover bg-center bg-no-repeat transition-[background-image] duration-500 ease-out"

src/components/floating-window.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from "react"
2+
import { X } from "@phosphor-icons/react"
23
import { cn } from "@/lib/utils"
34

45
export type FloatingWindowProps = {
@@ -7,14 +8,17 @@ export type FloatingWindowProps = {
78
children?: React.ReactNode
89
active?: boolean
910
onActivate?: () => void
11+
onClose?: () => void
1012
className?: string
1113
}
1214

1315
export function FloatingWindow({
1416
title,
1517
subtitle,
1618
children,
19+
active,
1720
onActivate,
21+
onClose,
1822
className,
1923
}: FloatingWindowProps) {
2024
return (
@@ -31,7 +35,9 @@ export function FloatingWindow({
3135
}}
3236
className={cn(
3337
"group rounded-[12px] w-full h-full flex flex-col",
34-
"bg-[var(--bg-overlay)]/70 backdrop-blur-md hover:bg-[var(--bg-overlay)] hover:backdrop-blur-none",
38+
"bg-[var(--bg-overlay)]/70 backdrop-blur-md",
39+
active && "max-md:bg-[var(--bg-overlay)] max-md:backdrop-blur-none",
40+
"hover:bg-[var(--bg-overlay)] hover:backdrop-blur-none",
3541
"border border-[var(--border-inactive)] hover-aurora",
3642
"shadow-[0_4px_6px_rgba(0,0,0,0.4),0_8px_20px_rgba(0,0,0,0.45)]",
3743
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-[rgba(184,127,255,0.25)]",
@@ -42,7 +48,7 @@ export function FloatingWindow({
4248
<div
4349
data-swapy-handle
4450
className={cn(
45-
"flex h-9 items-center gap-2 px-[14px] chrome-no-select cursor-grab transition-colors duration-150 rounded-t-[12px]",
51+
"flex h-9 max-md:h-11 items-center gap-2 px-[14px] max-md:px-3 chrome-no-select cursor-grab transition-colors duration-150 rounded-t-[12px]",
4652
"bg-[var(--bg-surface)]/60 backdrop-blur-sm group-hover:bg-[var(--bg-surface)]",
4753
"text-[var(--text-subtle)] group-hover:text-[var(--neon-violet)]",
4854
)}
@@ -56,6 +62,22 @@ export function FloatingWindow({
5662
</span>
5763
</>
5864
)}
65+
66+
<div className="flex-1" />
67+
68+
{onClose && (
69+
<button
70+
onClick={(e) => {
71+
e.stopPropagation()
72+
onClose()
73+
}}
74+
className="md:hidden flex items-center justify-center rounded-md p-1 text-[var(--text-subtle)] transition-colors hover:text-[var(--neon-red)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--neon-violet)]"
75+
aria-label={`Close ${title}`}
76+
style={{ minWidth: "36px", minHeight: "36px" }}
77+
>
78+
<X size={14} weight="bold" />
79+
</button>
80+
)}
5981
</div>
6082

6183
<div className="flex-1 min-h-0 overflow-hidden rounded-b-[12px]">{children}</div>

src/components/keybind-help.tsx

Lines changed: 137 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,57 @@ const CATEGORIES: KeybindCategory[] = [
3838
},
3939
]
4040

41+
type GestureEntry = {
42+
gesture: string
43+
action: string
44+
}
45+
46+
type GestureCategory = {
47+
title: string
48+
items: GestureEntry[]
49+
}
50+
51+
const TOUCH_GESTURES: GestureCategory[] = [
52+
{
53+
title: "Navigation",
54+
items: [
55+
{ gesture: "← swipe →", action: "Switch workspace" },
56+
{ gesture: "Tap 1–6", action: "Go to workspace" },
57+
],
58+
},
59+
{
60+
title: "Windows",
61+
items: [
62+
{ gesture: "Tap window", action: "Focus" },
63+
{ gesture: "✕ close", action: "Close window" },
64+
{ gesture: "Drag header", action: "Rearrange windows" },
65+
],
66+
},
67+
{
68+
title: "Overlays",
69+
items: [
70+
{ gesture: "Tap outside", action: "Close" },
71+
],
72+
},
73+
]
74+
75+
function useMediaQuery(query: string): boolean {
76+
const [matches, setMatches] = React.useState(() => {
77+
if (typeof window === "undefined") return false
78+
return window.matchMedia(query).matches
79+
})
80+
81+
React.useEffect(() => {
82+
const mq = window.matchMedia(query)
83+
const handler = (e: MediaQueryListEvent) => setMatches(e.matches)
84+
setMatches(mq.matches)
85+
mq.addEventListener("change", handler)
86+
return () => mq.removeEventListener("change", handler)
87+
}, [query])
88+
89+
return matches
90+
}
91+
4192
function useFocusTrap(
4293
containerRef: React.RefObject<HTMLElement | null>,
4394
isActive: boolean,
@@ -76,6 +127,7 @@ function useFocusTrap(
76127
export function KeybindHelp() {
77128
const { state, dispatch } = useWorkspace()
78129
const containerRef = React.useRef<HTMLDivElement>(null)
130+
const isMobile = useMediaQuery("(max-width: 767px)")
79131

80132
React.useEffect(() => {
81133
if (state.helpOpen) {
@@ -108,9 +160,9 @@ export function KeybindHelp() {
108160
ref={containerRef}
109161
role="dialog"
110162
aria-modal="true"
111-
aria-label="Keybind reference"
163+
aria-label={isMobile ? "Touch gestures" : "Keybind reference"}
112164
tabIndex={-1}
113-
className="fixed inset-0 z-200 flex items-start justify-center pt-[12vh] outline-none"
165+
className="fixed inset-0 z-200 flex items-start justify-center pt-[12vh] max-md:pt-0 max-md:items-end max-md:pb-0 outline-none"
114166
style={{
115167
background: "rgba(26,21,16,0.70)",
116168
backdropFilter: "blur(12px)",
@@ -121,12 +173,20 @@ export function KeybindHelp() {
121173
}}
122174
>
123175
<div
124-
className="w-full max-w-[520px] overflow-hidden rounded-[12px] shadow-2xl"
176+
className="w-full max-w-[520px] max-md:max-w-full max-md:rounded-b-none overflow-hidden rounded-[12px] shadow-2xl max-md:max-h-[85vh] max-md:overflow-y-auto"
125177
style={{
126178
background: "var(--bg-overlay)",
127179
border: "1px solid rgba(184,127,255,0.15)",
128180
}}
129181
>
182+
{/* Drag handle for mobile swipe-down */}
183+
<div className="hidden max-md:flex justify-center pt-2 pb-0">
184+
<div
185+
className="h-1 w-10 rounded-full"
186+
style={{ background: "rgba(245,230,200,0.2)" }}
187+
/>
188+
</div>
189+
130190
<div
131191
className="flex items-center gap-3 px-5 py-4"
132192
style={{
@@ -146,55 +206,96 @@ export function KeybindHelp() {
146206
className="text-sm font-medium"
147207
style={{ color: "var(--text-primary)" }}
148208
>
149-
Keybinds
209+
{isMobile ? "Touch Gestures" : "Keybinds"}
150210
</span>
151211
</div>
152212

153-
<div className="max-h-[50vh] overflow-y-auto px-5 py-3">
154-
{CATEGORIES.map((cat) => (
155-
<div key={cat.title} className="mb-4 last:mb-0">
156-
<div
157-
className="mb-2 text-[11px] font-semibold uppercase tracking-wider"
158-
style={{ color: "var(--neon-violet)" }}
159-
>
160-
{cat.title}
161-
</div>
162-
163-
{cat.binds.map((bind) => (
213+
{/* ─── Mobile: touch gestures ─── */}
214+
{isMobile ? (
215+
<div className="max-h-[55vh] overflow-y-auto px-5 py-3 overflow-scroll-touch">
216+
{TOUCH_GESTURES.map((cat) => (
217+
<div key={cat.title} className="mb-5 last:mb-0">
164218
<div
165-
key={bind.keys}
166-
className="flex items-center justify-between rounded-md px-3 py-2 text-sm"
167-
style={{
168-
borderBottom: "1px solid rgba(245,230,200,0.04)",
169-
}}
219+
className="mb-2 text-[11px] font-semibold uppercase tracking-wider"
220+
style={{ color: "var(--neon-violet)" }}
170221
>
171-
<span style={{ color: "var(--text-subtle)" }}>
172-
{bind.action}
173-
</span>
174-
<kbd
175-
className="rounded-md px-2.5 py-1 text-xs font-mono tracking-wide"
222+
{cat.title}
223+
</div>
224+
225+
{cat.items.map((item) => (
226+
<div
227+
key={item.gesture}
228+
className="flex items-center justify-between rounded-md px-3 py-2.5 text-sm"
176229
style={{
177-
background: "rgba(184,127,255,0.08)",
178-
color: "var(--neon-teal)",
179-
border: "1px solid rgba(0,229,200,0.12)",
230+
borderBottom: "1px solid rgba(245,230,200,0.04)",
180231
}}
181232
>
182-
{bind.keys}
183-
</kbd>
233+
<span style={{ color: "var(--text-subtle)" }}>
234+
{item.action}
235+
</span>
236+
<span
237+
className="rounded-md px-2.5 py-1 text-xs font-mono tracking-wide whitespace-nowrap"
238+
style={{
239+
background: "rgba(184,127,255,0.08)",
240+
color: "var(--neon-teal)",
241+
border: "1px solid rgba(0,229,200,0.12)",
242+
}}
243+
>
244+
{item.gesture}
245+
</span>
246+
</div>
247+
))}
248+
</div>
249+
))}
250+
</div>
251+
) : (
252+
/* ─── Desktop: keybinds ─── */
253+
<div className="max-h-[50vh] overflow-y-auto px-5 py-3">
254+
{CATEGORIES.map((cat) => (
255+
<div key={cat.title} className="mb-4 last:mb-0">
256+
<div
257+
className="mb-2 text-[11px] font-semibold uppercase tracking-wider"
258+
style={{ color: "var(--neon-violet)" }}
259+
>
260+
{cat.title}
184261
</div>
185-
))}
186-
</div>
187-
))}
188-
</div>
262+
263+
{cat.binds.map((bind) => (
264+
<div
265+
key={bind.keys}
266+
className="flex items-center justify-between rounded-md px-3 py-2 text-sm"
267+
style={{
268+
borderBottom: "1px solid rgba(245,230,200,0.04)",
269+
}}
270+
>
271+
<span style={{ color: "var(--text-subtle)" }}>
272+
{bind.action}
273+
</span>
274+
<kbd
275+
className="rounded-md px-2.5 py-1 text-xs font-mono tracking-wide"
276+
style={{
277+
background: "rgba(184,127,255,0.08)",
278+
color: "var(--neon-teal)",
279+
border: "1px solid rgba(0,229,200,0.12)",
280+
}}
281+
>
282+
{bind.keys}
283+
</kbd>
284+
</div>
285+
))}
286+
</div>
287+
))}
288+
</div>
289+
)}
189290

190291
<div
191-
className="flex items-center gap-4 px-5 py-2.5 text-[11px]"
292+
className="flex items-center gap-4 px-5 py-2.5 text-[11px] max-md:pb-[calc(0.625rem+56px)]"
192293
style={{
193294
color: "var(--text-subtle)",
194295
borderTop: "1px solid rgba(184,127,255,0.08)",
195296
}}
196297
>
197-
<span>Esc close</span>
298+
<span>{isMobile ? "Tap outside or swipe down" : "Esc"} close</span>
198299
</div>
199300
</div>
200301
</div>

src/components/rofi-search.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function RofiSearch() {
185185
role="dialog"
186186
aria-modal="true"
187187
aria-label="Rofi launcher"
188-
className="fixed inset-0 z-200 flex items-start justify-center pt-[15vh]"
188+
className="fixed inset-0 z-200 flex items-start justify-center pt-[15vh] max-md:pt-0 max-md:items-end max-md:pb-0"
189189
style={{
190190
background: "rgba(26,21,16,0.70)",
191191
backdropFilter: "blur(12px)",
@@ -196,12 +196,20 @@ export function RofiSearch() {
196196
}}
197197
>
198198
<div
199-
className="w-full max-w-[480px] overflow-hidden rounded-[12px] shadow-2xl"
199+
className="w-full max-w-[480px] max-md:max-w-full max-md:rounded-b-none overflow-hidden rounded-[12px] shadow-2xl max-md:max-h-[85vh] max-md:overflow-y-auto"
200200
style={{
201201
background: "var(--bg-overlay)",
202202
border: "1px solid rgba(184,127,255,0.15)",
203203
}}
204204
>
205+
{/* Drag handle for mobile swipe-down */}
206+
<div className="hidden max-md:flex justify-center pt-2 pb-1">
207+
<div
208+
className="h-1 w-10 rounded-full"
209+
style={{ background: "rgba(245,230,200,0.2)" }}
210+
/>
211+
</div>
212+
205213
<div className="relative">
206214
<span
207215
className="pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-sm"
@@ -311,7 +319,7 @@ export function RofiSearch() {
311319
</div>
312320

313321
<div
314-
className="flex items-center gap-4 px-4 py-2 text-[11px]"
322+
className="flex items-center gap-4 px-4 py-2 text-[11px] max-md:pb-[calc(0.5rem+56px)]"
315323
style={{
316324
color: "var(--text-subtle)",
317325
borderTop: "1px solid rgba(184,127,255,0.08)",

0 commit comments

Comments
 (0)