mirror of
https://github.com/ChiChou/entdb.git
synced 2026-06-10 23:07:47 +02:00
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:
+24
-24
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user