From 2dc43128b7db36425b3bf7c0b7677d033368be21 Mon Sep 17 00:00:00 2001 From: chaewon-huh Date: Tue, 14 Jul 2026 15:20:35 +0900 Subject: [PATCH 1/6] feat: make memo usable on mobile viewports Add a notes drawer with touch targets, safe-area layout, and 16px editor text so phones can browse and write without desktop shortcuts. Co-authored-by: Cursor --- app/globals.css | 170 +++++++++++++++++++++++++++++++++++++++- app/layout.tsx | 17 +++- components/icons.tsx | 40 ++++++++++ components/memo-app.tsx | 79 ++++++++++++++++--- 4 files changed, 294 insertions(+), 12 deletions(-) diff --git a/app/globals.css b/app/globals.css index 7df8640..9fcb83d 100644 --- a/app/globals.css +++ b/app/globals.css @@ -90,6 +90,7 @@ html, body { height: 100%; + height: 100dvh; } body { @@ -101,6 +102,7 @@ body { line-height: 1.4; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; + -webkit-tap-highlight-color: transparent; } button, @@ -148,10 +150,36 @@ button:disabled { display: flex; flex-direction: column; height: 100%; + height: 100dvh; overflow: hidden; background: var(--c-editor); color: var(--c-text); font-family: var(--c-ui-font); + padding: + env(safe-area-inset-top, 0) + env(safe-area-inset-right, 0) + env(safe-area-inset-bottom, 0) + env(safe-area-inset-left, 0); +} + +.zed-panel-backdrop { + display: none; +} + +.zed-mobile-bar { + display: none; +} + +.zed-panel__close { + display: none; +} + +.zed-empty__actions { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 8px; } .zed-titlebar { @@ -448,10 +476,15 @@ button:disabled { /* Login — elevated surface dialog on editor canvas */ .zed-login { min-height: 100%; + min-height: 100dvh; display: flex; align-items: center; justify-content: center; - padding: 24px; + padding: + max(24px, env(safe-area-inset-top, 0px)) + max(24px, env(safe-area-inset-right, 0px)) + max(24px, env(safe-area-inset-bottom, 0px)) + max(24px, env(safe-area-inset-left, 0px)); background: var(--c-editor); } @@ -742,3 +775,138 @@ button:disabled { white-space: nowrap; border: 0; } + +@media (max-width: 768px) { + :root { + --c-ui-size: 14px; + --c-buffer-size: 16px; + --c-gutter-w: 0px; + } + + .zed-panel-backdrop { + display: block; + position: fixed; + inset: 0; + z-index: 30; + border: 0; + background: rgba(0, 0, 0, 0.45); + } + + .zed-panel { + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 40; + width: min(var(--c-panel-w), 86vw); + max-width: 320px; + border-right: 1px solid var(--c-border-variant); + box-shadow: 12px 0 40px rgba(0, 0, 0, 0.35); + transform: translateX(0); + transition: + transform 180ms ease, + width 180ms ease, + border-color 180ms ease; + padding-top: env(safe-area-inset-top, 0); + padding-bottom: env(safe-area-inset-bottom, 0); + } + + .zed-panel[data-open="false"] { + width: min(var(--c-panel-w), 86vw); + max-width: 320px; + border-right-color: var(--c-border-variant); + transform: translateX(-105%); + pointer-events: none; + } + + .zed-panel__close { + display: inline-flex; + } + + .zed-mobile-bar { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + min-height: 48px; + padding: 8px 10px; + border-bottom: 1px solid var(--c-border-variant); + background: var(--c-surface); + } + + .zed-mobile-bar__title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--c-text); + font-size: 14px; + font-weight: 600; + } + + .zed-mobile-bar__btn, + .zed-icon-btn { + width: 40px; + height: 40px; + border-radius: 8px; + } + + .zed-gutter { + display: none; + } + + .zed-editor__body { + padding: 16px 16px 48px; + white-space: pre-wrap; + overflow-wrap: anywhere; + } + + .zed-note-item__delete { + display: inline-flex; + } + + .zed-note-item__hit { + min-height: 48px; + padding: 12px 36px 12px 12px; + } + + .zed-settings-btn { + min-height: 44px; + } + + .zed-empty__hint--desktop { + display: none; + } + + .zed-field, + .zed-btn { + min-height: 44px; + font-size: 16px; + } + + .zed-dialog { + max-width: 100%; + } + + .memo-settings-root { + align-items: stretch; + padding: + max(12px, env(safe-area-inset-top, 0px)) + max(12px, env(safe-area-inset-right, 0px)) + max(12px, env(safe-area-inset-bottom, 0px)) + max(12px, env(safe-area-inset-left, 0px)); + } + + .memo-settings { + width: 100%; + max-height: none; + flex: 1; + } +} + +@media (min-width: 769px) { + .zed-empty__actions .zed-btn:first-child { + display: none; + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 78068f2..2115bb8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { ThemeBoot } from "@/components/theme-boot"; import { faviconBootScript } from "@/lib/themes"; import "./globals.css"; @@ -9,6 +9,21 @@ export const metadata: Metadata = { icons: { icon: [{ url: "/favicon.svg", type: "image/svg+xml" }], }, + appleWebApp: { + capable: true, + title: "memo", + statusBarStyle: "black-translucent", + }, +}; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + themeColor: [ + { media: "(prefers-color-scheme: dark)", color: "#141414" }, + { media: "(prefers-color-scheme: light)", color: "#f3f3f3" }, + ], }; export default function RootLayout({ diff --git a/components/icons.tsx b/components/icons.tsx index 3bcb6e4..4db5201 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -23,6 +23,46 @@ export function PlusIcon({ size = 14, className }: IconProps) { ); } +export function MenuIcon({ size = 16, className }: IconProps) { + return ( + + + + ); +} + +export function CloseIcon({ size = 16, className }: IconProps) { + return ( + + + + ); +} + /** Heroicons outline `cog-6-tooth` (MIT) — https://heroicons.com */ export function SettingsIcon({ size = 16, className }: IconProps) { return ( diff --git a/components/memo-app.tsx b/components/memo-app.tsx index 29dba23..a165734 100644 --- a/components/memo-app.tsx +++ b/components/memo-app.tsx @@ -18,13 +18,20 @@ import { openSyncChannel, type SyncMessage, } from "@/lib/tab-sync"; -import { PlusIcon, SettingsIcon } from "./icons"; +import { CloseIcon, MenuIcon, PlusIcon, SettingsIcon } from "./icons"; import { SettingsPanel } from "./settings-panel"; type SaveState = "saved" | "saving" | "dirty" | "error"; const POLL_MS = 1500; const DRAFT_BROADCAST_MS = 32; +const NARROW_QUERY = "(max-width: 768px)"; + +function isNarrowViewport() { + return ( + typeof window !== "undefined" && window.matchMedia(NARROW_QUERY).matches + ); +} function previewTitle(note: Pick) { const fromTitle = note.title.trim(); @@ -244,6 +251,7 @@ export function MemoApp({ initialNotes }: { initialNotes: Note[] }) { setBody(note.body); setSaveState("saved"); setCaret(0); + if (isNarrowViewport()) setSidebarOpen(false); requestAnimationFrame(() => bodyRef.current?.focus()); }, []); @@ -416,7 +424,7 @@ export function MemoApp({ initialNotes }: { initialNotes: Note[] }) { note: data.note, }); selectNote(data.note); - setSidebarOpen(true); + if (!isNarrowViewport()) setSidebarOpen(true); }, [selectNote]); async function removeNote(id: string) { @@ -473,9 +481,21 @@ export function MemoApp({ initialNotes }: { initialNotes: Note[] }) { gutterRef.current.scrollTop = bodyRef.current.scrollTop; } + const activeTitle = activeId + ? previewTitle({ title: deriveTitle(body), body }) + : "memo"; + return (
+ {sidebarOpen ? ( + +