From bb72bd3f57d5b0c0af5405e25c0c8ccfc1523499 Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 15 Apr 2026 16:12:05 +0200 Subject: [PATCH] Fix mobile layout for keys page and responsive diff view - Fix virtualizer row height estimation by tracking actual column count - Batch consecutive single keys into grid rows for better space usage - Default diff view to unified mode on narrow screens (<480px) --- src/app/os/keys/page.tsx | 91 ++++++++++++++++++++++------------ src/components/diff-viewer.tsx | 4 +- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/app/os/keys/page.tsx b/src/app/os/keys/page.tsx index d1ebfff..b924d7e 100644 --- a/src/app/os/keys/page.tsx +++ b/src/app/os/keys/page.tsx @@ -9,6 +9,28 @@ import { Search, X, ChevronRight, ChevronDown } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; +function useColumnCount() { + const [cols, setCols] = useState(3); + useEffect(() => { + let timeout: ReturnType; + const update = () => { + const w = window.innerWidth; + setCols(w < 640 ? 1 : w < 1024 ? 2 : 3); + }; + const debouncedUpdate = () => { + clearTimeout(timeout); + timeout = setTimeout(update, 100); + }; + update(); + window.addEventListener("resize", debouncedUpdate); + return () => { + clearTimeout(timeout); + window.removeEventListener("resize", debouncedUpdate); + }; + }, []); + return cols; +} + import { createEngine } from "@/lib/engine"; interface GroupedKeys { @@ -74,7 +96,7 @@ const KeyBadge = memo(function KeyBadge({ }); interface RowItem { - type: "header" | "keys" | "single"; + type: "header" | "keys" | "singles"; prefix: string; keys: string[]; isOpen: boolean; @@ -92,6 +114,7 @@ export default function Keys() { const [openGroups, setOpenGroups] = useState>(new Set()); const parentRef = useRef(null); + const cols = useColumnCount(); // Debounce keyword with 300ms delay useEffect(() => { @@ -130,20 +153,32 @@ export default function Keys() { // Build flat list of rows for virtualization const rows = useMemo(() => { const result: RowItem[] = []; + let pendingSingles: string[] = []; + + const flushSingles = () => { + if (pendingSingles.length > 0) { + result.push({ + type: "singles", + prefix: "", + keys: pendingSingles, + isOpen: true, + }); + pendingSingles = []; + } + }; + for (const prefix of sortedPrefixes) { const keys = grouped[prefix]; - // Single key that equals its prefix - show as simple item, not collapsible + // Single key that equals its prefix - batch with other singles if (keys.length === 1 && keys[0] === prefix) { - result.push({ - type: "single", - prefix, - keys, - isOpen: true, - }); + pendingSingles.push(keys[0]); continue; } + // Flush any pending singles before adding a group + flushSingles(); + const isOpen = openGroups.has(prefix) || keys.length <= 8 || debouncedKeyword.length > 0; result.push({ @@ -162,6 +197,10 @@ export default function Keys() { }); } } + + // Flush remaining singles + flushSingles(); + return result; }, [sortedPrefixes, grouped, openGroups, debouncedKeyword]); @@ -170,11 +209,9 @@ export default function Keys() { getScrollElement: () => parentRef.current, estimateSize: (index) => { const row = rows[index]; - if (row.type === "single") return 36; if (row.type === "header") return 40; - // Estimate based on number of keys (3 columns, ~32px per row) - const keyRows = Math.ceil(row.keys.length / 3); - return keyRows * 32 + 24; // padding + const keyRows = Math.ceil(row.keys.length / cols); + return keyRows * 32 + 16; }, overscan: 5, }); @@ -282,7 +319,10 @@ export default function Keys() { />
-
+
{group.items.map((width, i) => (
{ const row = rows[virtualRow.index]; - if (row.type === "single") { - return ( -
- -
- ); - } - if (row.type === "header") { const isOpen = row.isOpen; return ( @@ -371,6 +392,7 @@ export default function Keys() { ); } + const isGrouped = row.type === "keys"; return (
-
+
{row.keys.map((key) => ( ))} diff --git a/src/components/diff-viewer.tsx b/src/components/diff-viewer.tsx index 6727645..e49dc2f 100644 --- a/src/components/diff-viewer.tsx +++ b/src/components/diff-viewer.tsx @@ -136,7 +136,9 @@ export function DiffViewer({ oldLabel, newLabel, }: DiffViewerProps) { - const [viewMode, setViewMode] = useState<"unified" | "split">("split"); + const [viewMode, setViewMode] = useState<"unified" | "split">( + typeof window !== "undefined" && window.innerWidth < 480 ? "unified" : "split" + ); const keysDiff = useMemo( () => diffPlistKeys(oldXml, newXml),