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
This commit is contained in:
cc
2026-04-15 17:59:10 +02:00
parent dc0efb7f7d
commit 320951a50f
3 changed files with 115 additions and 167 deletions
+24 -24
View File
@@ -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);
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="flex flex-col h-screen overflow-hidden">
<div className="flex flex-col min-h-screen">
<NavTop />
<Toaster />
<Suspense>
+90 -142
View File
@@ -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 (
<div className="mb-2">
<button
onClick={onToggle}
className="flex items-center gap-2 px-2 py-2 hover:bg-accent rounded transition-colors text-left w-full"
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
)}
<span className="font-mono text-sm text-muted-foreground">
{prefix}
<span className="text-foreground font-medium">.*</span>
</span>
<span className="text-xs text-muted-foreground bg-background border px-1.5 py-0.5 rounded-full">
{keys.length}
</span>
</button>
{expanded && (
<div
className="grid gap-x-6 gap-y-1 pl-8 pb-3 pr-2"
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
>
{keys.map((key) => (
<KeyBadge key={key} keyName={key} prefix={prefix} os={os} />
))}
</div>
)}
</div>
);
});
export default function Keys() {
const params = useSearchParams();
@@ -113,10 +158,8 @@ export default function Keys() {
const [debouncedKeyword, setDebouncedKeyword] = useState("");
const [openGroups, setOpenGroups] = useState<Set<string>>(new Set());
const parentRef = useRef<HTMLDivElement>(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 (
<div className="flex flex-col h-full">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 mb-4 shrink-0">
@@ -346,76 +340,30 @@ export default function Keys() {
)}
</div>
) : (
<div ref={parentRef} className="flex-1 min-h-0 overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: "100%",
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualRow) => {
const row = rows[virtualRow.index];
<div className="flex-1 overflow-auto">
{singles.length > 0 && (
<div
className="grid gap-x-6 gap-y-1 px-2 pb-4"
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
>
{singles.map((key) => (
<KeyBadge key={key} keyName={key} prefix="" os={os} />
))}
</div>
)}
if (row.type === "header") {
const isOpen = row.isOpen;
return (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<button
onClick={() => toggleGroup(row.prefix)}
className="flex items-center gap-2 px-2 py-2 hover:bg-accent rounded transition-colors text-left w-full"
>
{isOpen ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
)}
<span className="font-mono text-sm text-muted-foreground">
{row.prefix}
<span className="text-foreground font-medium">.*</span>
</span>
<span className="text-xs text-muted-foreground bg-background border px-1.5 py-0.5 rounded-full">
{row.keys.length}
</span>
</button>
</div>
);
}
const isGrouped = row.type === "keys";
return (
<div
key={virtualRow.key}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
transform: `translateY(${virtualRow.start}px)`,
}}
>
<div
className={`grid gap-x-6 gap-y-1 pr-2 ${isGrouped ? "pl-8 pb-3" : "px-2 pb-2"}`}
style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}
>
{row.keys.map((key) => (
<KeyBadge key={key} keyName={key} prefix={row.prefix} os={os} />
))}
</div>
</div>
);
})}
</div>
{groups.map(({ prefix, keys: groupKeys }) => (
<KeyGroup
key={prefix}
prefix={prefix}
keys={groupKeys}
os={os}
cols={cols}
isOpen={openGroups.has(prefix)}
onToggle={() => toggleGroup(prefix)}
isFiltering={isFiltering}
/>
))}
</div>
)}
</div>