feat(studio): polish search highlight, zebra rows, and transitions
CI / check (push) Successful in 2m24s
CI / deploy (push) Successful in 1m22s
CI / publish-container (push) Successful in 45s

Highlight the active search term in names/classnames (grid + table), add zebra striping and a left accent bar on selected table rows, fade the results list when switching grid/table or on first load, and swap the broken-icon fallback for a cleaner placeholder.
This commit is contained in:
openhands committed 2026-09-13 12:36:20 +02:00
1 parent 2a708b01b1
commit af1a06b0a3
2 files changed
+46 -9

No files matched your search

+45 -7
View File
@@ -25,6 +25,7 @@ import {
Trash2,
X,
} from "lucide-react";
import { motion } from "motion/react";
import {
lazy,
Suspense,
@@ -142,6 +143,23 @@ function writeLs(key: string, value: string) {
}
}
// Wraps the first case-insensitive match of `query` inside `text` in an accent
// mark so users can see where their search hit.
function Highlighted({ text, query }: { text: string; query: string }) {
if (!query) return text;
const index = text.toLowerCase().indexOf(query.toLowerCase());
if (index === -1) return text;
return (
<>
{text.slice(0, index)}
<mark className="rounded-[2px] bg-[var(--admin-accent)]/25 text-inherit">
{text.slice(index, index + query.length)}
</mark>
{text.slice(index + query.length)}
</>
);
}
// Table layout: a shared grid template keeps the sticky header and each
// virtualized row in perfect column alignment.
const TABLE_COLS =
@@ -1930,7 +1948,12 @@ export function StudioClient({
</p>
</div>
) : (
<>
<motion.div
key={viewMode}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.15 }}
>
{viewMode === "grid" ? (
<div
className="relative w-full"
@@ -1995,10 +2018,16 @@ export function StudioClient({
<div className="flex items-start justify-between gap-1 border-t border-[var(--admin-border)] px-2 py-1.5">
<div className="min-w-0">
<p className="truncate text-xs font-semibold text-[var(--admin-text)]">
{item.name}
<Highlighted
text={item.name}
query={activeSearch}
/>
</p>
<p className="truncate font-mono text-[0.6rem] text-[var(--admin-text-muted)]">
{item.classname}
<Highlighted
text={item.classname}
query={activeSearch}
/>
</p>
<p className="truncate text-[0.6rem] text-[var(--admin-text-muted)]/80">
{item.category || "—"}
@@ -2164,7 +2193,10 @@ export function StudioClient({
aria-selected={isSel}
className={cn(
"absolute left-0 top-0 grid w-full min-w-[660px] text-sm transition-colors hover:bg-[var(--admin-accent)]/5",
isSel && "bg-[var(--admin-accent)]/10",
vi.index % 2 === 1 &&
"bg-[var(--admin-text)]/[0.03]",
isSel &&
"bg-[var(--admin-accent)]/10 shadow-[inset_2px_0_0_0_var(--admin-accent)]",
)}
style={{
display: "grid",
@@ -2206,10 +2238,16 @@ export function StudioClient({
</button>
</td>
<td className="truncate border-t border-[var(--admin-border)] px-2 py-1.5 font-medium text-[var(--admin-text)]">
{item.name}
<Highlighted
text={item.name}
query={activeSearch}
/>
</td>
<td className="truncate border-t border-[var(--admin-border)] px-2 py-1.5 font-mono text-[0.7rem] text-[var(--admin-text-muted)]">
{item.classname}
<Highlighted
text={item.classname}
query={activeSearch}
/>
</td>
<td className="truncate border-t border-[var(--admin-border)] px-2 py-1.5 text-[var(--admin-text-muted)]">
{TYPE_LABELS[item.type] ?? item.type}
@@ -2354,7 +2392,7 @@ export function StudioClient({
</span>
)}
</div>
</>
</motion.div>
)}
</div>
@@ -28,9 +28,8 @@ export function handleImgError(
img.dataset.fallback = "2";
img.src = `/swf/dcr/hof_furni/icons/${encodeURIComponent(iconName)}_icon.png`;
} else {
img.style.opacity = "0.3";
img.src =
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="%23666" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>';
'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="%2399a0aa" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2.5"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/></svg>';
}
}