Skip to content

Commit ec56db0

Browse files
hyperb1issclaude
andcommitted
feat(web): responsive mobile layout with viewport-adaptive editor
Replace fixed 700px editor/preview heights with viewport-responsive sizing (40vh mobile → 50vh tablet → 70vh desktop). Add mobile hamburger nav with animated drawer, tighten spacing/padding across all sections, and add iOS-specific fixes (overscroll, textarea zoom, safe areas). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 80c32bc commit ec56db0

9 files changed

Lines changed: 170 additions & 42 deletions

File tree

web/src/app/globals.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,26 @@ body {
207207
.editor-scrollbar::-webkit-scrollbar-thumb:hover {
208208
background: var(--sc-fg-muted);
209209
}
210+
211+
/* ═══════════════════════════════════════════════
212+
Mobile Enhancements
213+
═══════════════════════════════════════════════ */
214+
215+
/* Prevent overscroll bounce on iOS */
216+
html {
217+
overscroll-behavior: none;
218+
}
219+
220+
/* Ensure textareas don't zoom on iOS (min 16px font) */
221+
@media (max-width: 639px) {
222+
textarea {
223+
font-size: 16px;
224+
}
225+
}
226+
227+
/* Safe area padding for notched devices */
228+
@supports (padding: env(safe-area-inset-bottom)) {
229+
footer {
230+
padding-bottom: env(safe-area-inset-bottom);
231+
}
232+
}

web/src/app/layout.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Metadata } from 'next';
1+
import type { Metadata, Viewport } from 'next';
22
import { Inter, JetBrains_Mono } from 'next/font/google';
33
import './globals.css';
44

@@ -14,6 +14,13 @@ const jetbrainsMono = JetBrains_Mono({
1414
display: 'swap',
1515
});
1616

17+
export const viewport: Viewport = {
18+
width: 'device-width',
19+
initialScale: 1,
20+
maximumScale: 1,
21+
viewportFit: 'cover',
22+
};
23+
1724
export const metadata: Metadata = {
1825
title: 'SilkPrint — Markdown to Stunning PDFs',
1926
description:

web/src/components/editor.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ export function Editor() {
177177
}, []);
178178

179179
return (
180-
<section id="editor" className="mx-auto max-w-[1600px] px-4 py-16 2xl:px-6">
181-
<div className="mb-8 text-center">
182-
<h2 className="mb-3 text-3xl font-bold tracking-tight md:text-4xl">
180+
<section id="editor" className="mx-auto max-w-[1600px] px-3 py-8 sm:px-4 lg:py-16 2xl:px-6">
181+
<div className="mb-6 text-center lg:mb-8">
182+
<h2 className="mb-2 text-2xl font-bold tracking-tight sm:text-3xl md:text-4xl lg:mb-3">
183183
<span className="gradient-text">Live Editor</span>
184184
</h2>
185185
<p className="text-sc-fg-muted">
@@ -191,12 +191,12 @@ export function Editor() {
191191
<ThemeSelector activeTheme={activeTheme} onSelect={setActiveTheme} />
192192

193193
{/* Editor / Preview split */}
194-
<div className="glow-border grid grid-cols-1 overflow-hidden rounded-2xl bg-sc-bg-dark lg:grid-cols-2">
194+
<div className="glow-border grid grid-cols-1 overflow-hidden rounded-xl bg-sc-bg-dark sm:rounded-2xl lg:grid-cols-2">
195195
{/* Markdown input */}
196196
<div className="flex flex-col border-b border-sc-cyan/10 lg:border-b-0 lg:border-r">
197-
<div className="flex items-center justify-between border-b border-sc-cyan/10 px-4 py-2.5">
197+
<div className="flex items-center justify-between border-b border-sc-cyan/10 px-3 py-2 sm:px-4 sm:py-2.5">
198198
<div className="flex items-center gap-2">
199-
<div className="flex gap-1.5">
199+
<div className="hidden gap-1.5 sm:flex">
200200
<span className="h-3 w-3 rounded-full bg-sc-error/60" />
201201
<span className="h-3 w-3 rounded-full bg-sc-warning/60" />
202202
<span className="h-3 w-3 rounded-full bg-sc-success/60" />
@@ -232,28 +232,28 @@ export function Editor() {
232232
onChange={handleFileUpload}
233233
className="hidden"
234234
/>
235-
<span className="font-mono text-xs text-sc-fg-dim">Markdown</span>
235+
<span className="hidden font-mono text-xs text-sc-fg-dim sm:inline">Markdown</span>
236236
</div>
237237
</div>
238238
<textarea
239239
value={markdown}
240240
onChange={e => setMarkdown(e.target.value)}
241-
className="editor-scrollbar h-[700px] w-full resize-none bg-transparent p-4 font-mono text-sm leading-relaxed text-sc-fg placeholder:text-sc-fg-dim focus:outline-none"
241+
className="editor-scrollbar h-[40vh] w-full resize-none bg-transparent p-3 font-mono text-sm leading-relaxed text-sc-fg placeholder:text-sc-fg-dim focus:outline-none sm:p-4 md:h-[50vh] lg:h-[70vh]"
242242
placeholder="Paste your Markdown here..."
243243
spellCheck={false}
244244
/>
245245
</div>
246246

247247
{/* Preview panel */}
248248
<div className="flex flex-col">
249-
<div className="flex items-center justify-between border-b border-sc-cyan/10 px-4 py-2.5">
250-
<div className="flex items-center gap-2">
251-
<span className="text-xs font-medium text-sc-fg-dim">Preview</span>
252-
<span className="rounded bg-sc-purple/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-sc-purple">
249+
<div className="flex items-center justify-between border-b border-sc-cyan/10 px-3 py-2 sm:px-4 sm:py-2.5">
250+
<div className="flex min-w-0 items-center gap-1.5 sm:gap-2">
251+
<span className="shrink-0 text-xs font-medium text-sc-fg-dim">Preview</span>
252+
<span className="truncate rounded bg-sc-purple/15 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-sc-purple sm:px-2">
253253
{activeTheme}
254254
</span>
255255
{engineState.status === 'rendering' && (
256-
<span className="animate-pulse rounded bg-sc-cyan/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-sc-cyan">
256+
<span className="hidden animate-pulse rounded bg-sc-cyan/15 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-sc-cyan sm:inline">
257257
Rendering...
258258
</span>
259259
)}
@@ -262,7 +262,7 @@ export function Editor() {
262262
type="button"
263263
onClick={handleDownload}
264264
disabled={!pdfBytes || pdfBytes.length === 0}
265-
className="group flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-sc-purple to-sc-coral px-3 py-1.5 text-xs font-semibold text-white transition-all hover:-translate-y-0.5 hover:shadow-[0_4px_15px_rgba(225,53,255,0.3)] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:translate-y-0 disabled:hover:shadow-none"
265+
className="group flex shrink-0 items-center gap-1 rounded-lg bg-gradient-to-r from-sc-purple to-sc-coral px-2.5 py-1.5 text-xs font-semibold text-white transition-all hover:-translate-y-0.5 hover:shadow-[0_4px_15px_rgba(225,53,255,0.3)] disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:translate-y-0 disabled:hover:shadow-none sm:gap-1.5 sm:px-3"
266266
>
267267
<svg
268268
aria-hidden="true"
@@ -283,7 +283,7 @@ export function Editor() {
283283
</div>
284284

285285
{/* Preview content */}
286-
<div className="editor-scrollbar h-[700px] overflow-y-auto">
286+
<div className="editor-scrollbar h-[40vh] overflow-y-auto md:h-[50vh] lg:h-[70vh]">
287287
{engineState.status === 'loading' && (
288288
<div className="flex h-full flex-col items-center justify-center gap-4 p-8">
289289
<LoadingSpinner />
@@ -313,7 +313,7 @@ export function Editor() {
313313
</div>
314314
)}
315315
{pdfBytes && pdfBytes.length > 0 ? (
316-
<PdfPreview pdfBytes={pdfBytes} className="p-4" />
316+
<PdfPreview pdfBytes={pdfBytes} className="p-2 sm:p-4" />
317317
) : (
318318
<div className="flex h-full items-center justify-center p-8">
319319
<LoadingSpinner />

web/src/components/features.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,21 @@ const FEATURES = [
128128

129129
export function Features() {
130130
return (
131-
<section className="mx-auto max-w-7xl px-6 py-20">
132-
<div className="mb-12 text-center">
133-
<h2 className="mb-3 text-3xl font-bold tracking-tight md:text-4xl">
131+
<section className="mx-auto max-w-7xl px-4 py-12 sm:px-6 md:py-20">
132+
<div className="mb-8 text-center md:mb-12">
133+
<h2 className="mb-2 text-2xl font-bold tracking-tight sm:text-3xl md:mb-3 md:text-4xl">
134134
Why <span className="gradient-text">SilkPrint</span>?
135135
</h2>
136136
<p className="mx-auto max-w-lg text-sc-fg-muted">
137137
Everything you need for beautiful document generation, nothing you don&apos;t.
138138
</p>
139139
</div>
140140

141-
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
141+
<div className="grid grid-cols-1 gap-4 sm:gap-6 md:grid-cols-2 lg:grid-cols-3">
142142
{FEATURES.map(feature => (
143143
<div
144144
key={feature.title}
145-
className="glow-border group rounded-2xl bg-sc-bg-dark p-6 transition-all hover:-translate-y-1"
145+
className="glow-border group rounded-xl bg-sc-bg-dark p-4 transition-all hover:-translate-y-1 sm:rounded-2xl sm:p-6"
146146
>
147147
<div className="mb-4 inline-flex rounded-xl bg-sc-purple/10 p-3 text-sc-purple transition-colors group-hover:bg-sc-purple/20">
148148
{feature.icon}

web/src/components/header.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
1+
'use client';
2+
13
import Link from 'next/link';
4+
import { useCallback, useEffect, useState } from 'react';
25

36
export function Header() {
7+
const [menuOpen, setMenuOpen] = useState(false);
8+
9+
// Close menu on resize past mobile breakpoint
10+
useEffect(() => {
11+
const mq = window.matchMedia('(min-width: 768px)');
12+
const handler = () => {
13+
if (mq.matches) setMenuOpen(false);
14+
};
15+
mq.addEventListener('change', handler);
16+
return () => mq.removeEventListener('change', handler);
17+
}, []);
18+
19+
// Lock body scroll when menu is open
20+
useEffect(() => {
21+
document.body.style.overflow = menuOpen ? 'hidden' : '';
22+
return () => {
23+
document.body.style.overflow = '';
24+
};
25+
}, [menuOpen]);
26+
27+
const closeMenu = useCallback(() => setMenuOpen(false), []);
28+
429
return (
530
<header className="glass fixed top-0 z-50 w-full">
6-
<div className="mx-auto flex max-w-7xl items-center justify-between px-6 py-4">
31+
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 md:px-6 md:py-4">
732
<Link href="/" className="flex items-center gap-2">
833
<span className="gradient-text text-xl font-bold tracking-tight">SilkPrint</span>
934
</Link>
1035

11-
<nav className="flex items-center gap-6">
36+
{/* Desktop nav */}
37+
<nav className="hidden items-center gap-6 md:flex">
1238
<a
1339
href="#editor"
1440
className="text-sm text-sc-fg-muted transition-colors hover:text-sc-cyan"
@@ -38,7 +64,72 @@ export function Header() {
3864
Install CLI
3965
</a>
4066
</nav>
67+
68+
{/* Mobile hamburger */}
69+
<button
70+
type="button"
71+
onClick={() => setMenuOpen(v => !v)}
72+
className="flex items-center justify-center rounded-lg p-2 text-sc-fg-muted transition-colors hover:bg-sc-bg-highlight hover:text-sc-fg md:hidden"
73+
aria-label={menuOpen ? 'Close menu' : 'Open menu'}
74+
>
75+
<svg
76+
aria-hidden="true"
77+
className="h-5 w-5"
78+
fill="none"
79+
viewBox="0 0 24 24"
80+
stroke="currentColor"
81+
strokeWidth={2}
82+
>
83+
{menuOpen ? (
84+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
85+
) : (
86+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
87+
)}
88+
</svg>
89+
</button>
4190
</div>
91+
92+
{/* Mobile drawer */}
93+
{menuOpen && (
94+
<nav className="animate-drop-in border-t border-sc-cyan/10 bg-sc-bg-dark/95 backdrop-blur-xl md:hidden">
95+
<div className="flex flex-col gap-1 px-4 py-4">
96+
{/* biome-ignore lint/a11y/useValidAnchor: hash nav + menu close */}
97+
<a
98+
href="#editor"
99+
onClick={closeMenu}
100+
className="rounded-lg px-3 py-2.5 text-sm font-medium text-sc-fg-muted transition-colors hover:bg-sc-bg-highlight hover:text-sc-cyan"
101+
>
102+
Editor
103+
</a>
104+
{/* biome-ignore lint/a11y/useValidAnchor: hash nav + menu close */}
105+
<a
106+
href="#themes"
107+
onClick={closeMenu}
108+
className="rounded-lg px-3 py-2.5 text-sm font-medium text-sc-fg-muted transition-colors hover:bg-sc-bg-highlight hover:text-sc-cyan"
109+
>
110+
Themes
111+
</a>
112+
<a
113+
href="https://github.com/hyperb1iss/silkprint"
114+
target="_blank"
115+
rel="noopener noreferrer"
116+
className="rounded-lg px-3 py-2.5 text-sm font-medium text-sc-fg-muted transition-colors hover:bg-sc-bg-highlight hover:text-sc-cyan"
117+
>
118+
GitHub
119+
</a>
120+
<div className="mt-2 border-t border-sc-cyan/10 pt-3">
121+
<a
122+
href="https://github.com/hyperb1iss/silkprint#installation"
123+
target="_blank"
124+
rel="noopener noreferrer"
125+
className="flex items-center justify-center rounded-lg bg-sc-bg-highlight px-4 py-2.5 text-sm font-medium text-sc-cyan transition-all hover:shadow-[0_0_20px_rgba(128,255,234,0.15)]"
126+
>
127+
Install CLI
128+
</a>
129+
</div>
130+
</div>
131+
</nav>
132+
)}
42133
</header>
43134
);
44135
}

web/src/components/hero.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export function Hero() {
22
return (
3-
<section className="relative flex min-h-[85vh] flex-col items-center justify-center overflow-hidden px-6 pt-20 text-center">
3+
<section className="relative flex min-h-[70vh] flex-col items-center justify-center overflow-hidden px-4 pt-16 text-center md:min-h-[85vh] md:px-6 md:pt-20">
44
{/* Background gradient orbs */}
55
<div className="pointer-events-none absolute inset-0">
66
<div className="absolute left-1/4 top-1/4 h-96 w-96 rounded-full bg-sc-purple/10 blur-[120px]" />
@@ -15,11 +15,11 @@ export function Hero() {
1515
40+ themes &middot; Powered by Typst
1616
</div>
1717

18-
<h1 className="mb-6 text-5xl font-extrabold leading-[1.1] tracking-tight md:text-7xl">
18+
<h1 className="mb-6 text-4xl font-extrabold leading-[1.1] tracking-tight sm:text-5xl md:text-7xl">
1919
Markdown to PDF, <span className="gradient-text-shimmer block">made stunning.</span>
2020
</h1>
2121

22-
<p className="mx-auto mb-10 max-w-2xl text-lg leading-relaxed text-sc-fg-muted md:text-xl">
22+
<p className="mx-auto mb-8 max-w-2xl text-base leading-relaxed text-sc-fg-muted sm:mb-10 md:text-xl">
2323
Paste your Markdown. Pick a gorgeous theme. Get a print-ready PDF in seconds. No LaTeX, no
2424
setup, no suffering.
2525
</p>

web/src/components/pdf-preview.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ export function PdfPreview({ pdfBytes, className = '' }: PdfPreviewProps) {
3434
const canvases: HTMLCanvasElement[] = [];
3535

3636
const containerWidth = containerRef.current?.clientWidth ?? 600;
37-
// Leave some padding
38-
const targetWidth = containerWidth - 32;
37+
// Tighter padding on small containers (mobile)
38+
const padding = containerWidth < 480 ? 16 : 32;
39+
const targetWidth = containerWidth - padding;
3940

4041
for (let i = 1; i <= doc.numPages; i++) {
4142
const page = await doc.getPage(i);

web/src/components/theme-gallery.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ const THEME_FAMILIES = [
6262

6363
export function ThemeGallery() {
6464
return (
65-
<section id="themes" className="mx-auto max-w-7xl px-6 py-20">
66-
<div className="mb-12 text-center">
67-
<h2 className="mb-3 text-3xl font-bold tracking-tight md:text-4xl">
65+
<section id="themes" className="mx-auto max-w-7xl px-4 py-12 sm:px-6 md:py-20">
66+
<div className="mb-8 text-center md:mb-12">
67+
<h2 className="mb-2 text-2xl font-bold tracking-tight sm:text-3xl md:mb-3 md:text-4xl">
6868
<span className="gradient-text">40+ Themes</span>
6969
</h2>
7070
<p className="mx-auto max-w-lg text-sc-fg-muted">

0 commit comments

Comments
 (0)