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,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
import {
|
import {
|
||||||
lazy,
|
lazy,
|
||||||
Suspense,
|
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
|
// Table layout: a shared grid template keeps the sticky header and each
|
||||||
// virtualized row in perfect column alignment.
|
// virtualized row in perfect column alignment.
|
||||||
const TABLE_COLS =
|
const TABLE_COLS =
|
||||||
@@ -1930,7 +1948,12 @@ export function StudioClient({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<motion.div
|
||||||
|
key={viewMode}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
{viewMode === "grid" ? (
|
{viewMode === "grid" ? (
|
||||||
<div
|
<div
|
||||||
className="relative w-full"
|
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="flex items-start justify-between gap-1 border-t border-[var(--admin-border)] px-2 py-1.5">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-xs font-semibold text-[var(--admin-text)]">
|
<p className="truncate text-xs font-semibold text-[var(--admin-text)]">
|
||||||
{item.name}
|
<Highlighted
|
||||||
|
text={item.name}
|
||||||
|
query={activeSearch}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate font-mono text-[0.6rem] text-[var(--admin-text-muted)]">
|
<p className="truncate font-mono text-[0.6rem] text-[var(--admin-text-muted)]">
|
||||||
{item.classname}
|
<Highlighted
|
||||||
|
text={item.classname}
|
||||||
|
query={activeSearch}
|
||||||
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-[0.6rem] text-[var(--admin-text-muted)]/80">
|
<p className="truncate text-[0.6rem] text-[var(--admin-text-muted)]/80">
|
||||||
{item.category || "—"}
|
{item.category || "—"}
|
||||||
@@ -2164,7 +2193,10 @@ export function StudioClient({
|
|||||||
aria-selected={isSel}
|
aria-selected={isSel}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-0 top-0 grid w-full min-w-[660px] text-sm transition-colors hover:bg-[var(--admin-accent)]/5",
|
"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={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
@@ -2206,10 +2238,16 @@ export function StudioClient({
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td className="truncate border-t border-[var(--admin-border)] px-2 py-1.5 font-medium text-[var(--admin-text)]">
|
<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>
|
||||||
<td className="truncate border-t border-[var(--admin-border)] px-2 py-1.5 font-mono text-[0.7rem] text-[var(--admin-text-muted)]">
|
<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>
|
||||||
<td className="truncate border-t border-[var(--admin-border)] px-2 py-1.5 text-[var(--admin-text-muted)]">
|
<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}
|
{TYPE_LABELS[item.type] ?? item.type}
|
||||||
@@ -2354,7 +2392,7 @@ export function StudioClient({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,8 @@ export function handleImgError(
|
|||||||
img.dataset.fallback = "2";
|
img.dataset.fallback = "2";
|
||||||
img.src = `/swf/dcr/hof_furni/icons/${encodeURIComponent(iconName)}_icon.png`;
|
img.src = `/swf/dcr/hof_furni/icons/${encodeURIComponent(iconName)}_icon.png`;
|
||||||
} else {
|
} else {
|
||||||
img.style.opacity = "0.3";
|
|
||||||
img.src =
|
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