From 320951a50f83ef940307c5f71598c8f238274f3e Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 15 Apr 2026 17:59:10 +0200 Subject: [PATCH] Simplify keys page layout, add subtle blue accent colors - Remove virtualization from keys page, use simple grid layout - Add min-h-0 to main element for proper flex behavior - Add subtle blue tint (oklch hue 250) to primary, accent, ring colors - Keep black/white backgrounds, only colorize interactive elements - Fix hover states to use foreground color instead of primary --- src/app/globals.css | 48 ++++---- src/app/layout.tsx | 2 +- src/app/os/keys/page.tsx | 232 +++++++++++++++------------------------ 3 files changed, 115 insertions(+), 167 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index a56193e..d9d76ac 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -51,18 +51,18 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(0.45 0.12 250); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --accent: oklch(0.96 0.02 250); + --accent-foreground: oklch(0.25 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --ring: oklch(0.55 0.12 250); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); @@ -70,12 +70,12 @@ --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary: oklch(0.45 0.12 250); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-accent: oklch(0.96 0.02 250); + --sidebar-accent-foreground: oklch(0.25 0 0); --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-ring: oklch(0.55 0.12 250); } .dark { @@ -85,18 +85,18 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); + --primary: oklch(0.65 0.12 250); + --primary-foreground: oklch(0.15 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); + --accent: oklch(0.28 0.03 250); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); + --ring: oklch(0.60 0.12 250); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); @@ -104,12 +104,12 @@ --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); + --sidebar-primary: oklch(0.65 0.12 250); + --sidebar-primary-foreground: oklch(0.15 0 0); + --sidebar-accent: oklch(0.28 0.03 250); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --sidebar-ring: oklch(0.60 0.12 250); } @layer base { @@ -125,22 +125,22 @@ :root { --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary: oklch(0.45 0.12 250); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-accent: oklch(0.96 0.02 250); + --sidebar-accent-foreground: oklch(0.25 0 0); --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-ring: oklch(0.55 0.12 250); } .dark { --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); + --sidebar-primary: oklch(0.65 0.12 250); + --sidebar-primary-foreground: oklch(0.15 0 0); + --sidebar-accent: oklch(0.28 0.03 250); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.439 0 0); + --sidebar-ring: oklch(0.60 0.12 250); } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 61a6a0c..66f68e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -35,7 +35,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > -
+
diff --git a/src/app/os/keys/page.tsx b/src/app/os/keys/page.tsx index b924d7e..be34aa8 100644 --- a/src/app/os/keys/page.tsx +++ b/src/app/os/keys/page.tsx @@ -1,8 +1,7 @@ "use client"; -import { useState, useEffect, useMemo, useCallback, memo, useRef } from "react"; +import { useState, useEffect, useMemo, useCallback, memo } from "react"; import { useSearchParams } from "next/navigation"; -import { useVirtualizer } from "@tanstack/react-virtual"; import Link from "next/link"; import { Search, X, ChevronRight, ChevronDown } from "lucide-react"; @@ -95,12 +94,58 @@ const KeyBadge = memo(function KeyBadge({ ); }); -interface RowItem { - type: "header" | "keys" | "singles"; +const KeyGroup = memo(function KeyGroup({ + prefix, + keys, + os, + cols, + isOpen, + onToggle, + isFiltering, +}: { prefix: string; keys: string[]; + os: string; + cols: number; isOpen: boolean; -} + onToggle: () => void; + isFiltering: boolean; +}) { + const autoExpand = keys.length <= 8 || isFiltering; + const expanded = isOpen || autoExpand; + + return ( +
+ + {expanded && ( +
+ {keys.map((key) => ( + + ))} +
+ )} +
+ ); +}); export default function Keys() { const params = useSearchParams(); @@ -113,10 +158,8 @@ export default function Keys() { const [debouncedKeyword, setDebouncedKeyword] = useState(""); const [openGroups, setOpenGroups] = useState>(new Set()); - const parentRef = useRef(null); const cols = useColumnCount(); - // Debounce keyword with 300ms delay useEffect(() => { const timer = setTimeout(() => { setDebouncedKeyword(keyword); @@ -150,72 +193,6 @@ export default function Keys() { [grouped] ); - // 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 - batch with other singles - if (keys.length === 1 && keys[0] === prefix) { - 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({ - type: "header", - prefix, - keys, - isOpen, - }); - - if (isOpen) { - result.push({ - type: "keys", - prefix, - keys, - isOpen: true, - }); - } - } - - // Flush remaining singles - flushSingles(); - - return result; - }, [sortedPrefixes, grouped, openGroups, debouncedKeyword]); - - const virtualizer = useVirtualizer({ - count: rows.length, - getScrollElement: () => parentRef.current, - estimateSize: (index) => { - const row = rows[index]; - if (row.type === "header") return 40; - const keyRows = Math.ceil(row.keys.length / cols); - return keyRows * 32 + 16; - }, - overscan: 5, - }); - const isFiltering = debouncedKeyword.length > 0; const toggleGroup = useCallback((prefix: string) => { @@ -238,6 +215,23 @@ export default function Keys() { setOpenGroups(new Set()); }, []); + // Separate single keys from groups + const { groups, singles } = useMemo(() => { + const groups: { prefix: string; keys: string[] }[] = []; + const singles: string[] = []; + + for (const prefix of sortedPrefixes) { + const prefixKeys = grouped[prefix]; + if (prefixKeys.length === 1 && prefixKeys[0] === prefix) { + singles.push(prefixKeys[0]); + } else { + groups.push({ prefix, keys: prefixKeys }); + } + } + + return { groups, singles }; + }, [sortedPrefixes, grouped]); + return (
@@ -346,76 +340,30 @@ export default function Keys() { )}
) : ( -
-
- {virtualizer.getVirtualItems().map((virtualRow) => { - const row = rows[virtualRow.index]; +
+ {singles.length > 0 && ( +
+ {singles.map((key) => ( + + ))} +
+ )} - if (row.type === "header") { - const isOpen = row.isOpen; - return ( -
- -
- ); - } - - const isGrouped = row.type === "keys"; - return ( -
-
- {row.keys.map((key) => ( - - ))} -
-
- ); - })} -
+ {groups.map(({ prefix, keys: groupKeys }) => ( + toggleGroup(prefix)} + isFiltering={isFiltering} + /> + ))}
)}