feat(studio): polish search highlight, zebra rows, and transitions
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:
1 parent
2a708b01b1
commit
af1a06b0a3
2 files changed
+46
-9
No files matched your search
@@ -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>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user