fix: few minor ui issues

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-19 19:31:50 +02:00
1 parent 83e0bebf45
commit 2f2d85018e
4 files changed
+101 -30

No files matched your search

@@ -27,7 +27,6 @@ import { useDebouncedValue } from "../../../hooks/useDebouncedValue";
const MIN_QUERY_LENGTH = 2;
const DEBOUNCE_MS = 250;
const DROPDOWN_WIDTH = 420;
type Props = {
onUnauthorized?: () => void;
@@ -47,8 +46,24 @@ export default function GlobalSearch({ onUnauthorized }: Props) {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<SearchResult | null>(null);
const [error, setError] = useState<string | null>(null);
// The dropdown's width is pinned to this (not a fixed constant) so it always exactly matches
// the search box, not just approximates it -- the box itself is fluid (TopBar's center grid
// column is `minmax(0, 480px)`, so its real width varies with the window). Tracked live via
// ResizeObserver rather than read once, so resizing the window (or collapsing the sidebar,
// which changes how much room the center column has) keeps them in sync.
const [anchorWidth, setAnchorWidth] = useState<number>();
const debouncedQuery = useDebouncedValue(query.trim(), DEBOUNCE_MS);
useEffect(() => {
const el = anchorRef.current;
if (!el) return;
const update = () => setAnchorWidth(el.offsetWidth);
update();
const observer = new ResizeObserver(update);
observer.observe(el);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (debouncedQuery.length < MIN_QUERY_LENGTH) {
setResult(null);
@@ -135,9 +150,9 @@ export default function GlobalSearch({ onUnauthorized }: Props) {
disableAutoFocus
disableEnforceFocus
disableRestoreFocus
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
transformOrigin={{ vertical: "top", horizontal: "center" }}
slotProps={{ paper: { sx: { width: DROPDOWN_WIDTH, maxWidth: "90vw", mt: 0.5, maxHeight: 480, overflow: "auto" } } }}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
slotProps={{ paper: { sx: { width: anchorWidth, mt: 0, maxHeight: 480, overflow: "auto" } } }}
>
{loading && (
<Stack alignItems="center" sx={{ py: 3 }}>
@@ -117,6 +117,11 @@ export default function Sidebar({ isAdmin, onSelectCategory, bookmarksVersion }:
// The bookmark id currently being dragged, if any -- set on that row's dragstart, read by every
// other row's drop handler, cleared once the gesture ends (drop, or a drag that's cancelled).
const [draggingId, setDraggingId] = useState<string | null>(null);
// Whichever row the pointer is currently over mid-drag -- purely visual (a highlighted drop
// target), so it's obvious where a drop will land instead of just a generic cursor. Updated on
// every dragover rather than dragenter/dragleave, which fire unreliably as the pointer crosses
// a row's own child elements (icon/text) due to event bubbling.
const [dragOverId, setDragOverId] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
@@ -148,6 +153,37 @@ export default function Sidebar({ isAdmin, onSelectCategory, bookmarksVersion }:
});
};
// Native HTML5 drag defaults to snapshotting the *entire* dragged element as the ghost image --
// here that's a full sidebar-width row (padding included, for a comfortable click target), not
// just the short label it visibly shows. Left alone, the ghost reads as much wider than "the
// item" the label suggests, and can visually overlap neighboring rows enough that it feels like
// they're being dragged too. Supplying a small custom drag image (just the label, sized to fit)
// keeps the ghost honest about what's actually moving. The temporary node is built off-screen
// and removed on the next frame -- setDragImage snapshots it synchronously when called, so it
// only needs to exist for that one instant.
const handleDragStart = (e: React.DragEvent<HTMLElement>, id: string, label: string) => {
setDraggingId(id);
const preview = document.createElement("div");
preview.textContent = label;
Object.assign(preview.style, {
position: "fixed",
top: "-1000px",
left: "-1000px",
padding: "4px 10px",
borderRadius: "6px",
background: "#1f2430",
color: "#fff",
fontSize: "13px",
fontFamily: "inherit",
whiteSpace: "nowrap",
boxShadow: "0 2px 8px rgba(0,0,0,.25)",
});
document.body.appendChild(preview);
e.dataTransfer.setDragImage(preview, 12, 14);
e.dataTransfer.effectAllowed = "move";
requestAnimationFrame(() => preview.remove());
};
const onDashboard = location.pathname === "/";
const onCollections = location.pathname.startsWith("/models/collections");
const onTags = location.pathname.startsWith("/models/tags");
@@ -352,17 +388,37 @@ export default function Sidebar({ isAdmin, onSelectCategory, bookmarksVersion }:
<ListItemButton
key={entry.id}
draggable
onDragStart={() => setDraggingId(entry.id)}
onDragOver={e => e.preventDefault()}
onDragStart={e => handleDragStart(e, entry.id, label)}
onDragOver={e => {
e.preventDefault();
if (draggingId && draggingId !== entry.id) setDragOverId(entry.id);
}}
onDrop={e => {
e.preventDefault();
if (draggingId) handleDrop(draggingId, entry.id);
setDraggingId(null);
setDragOverId(null);
}}
onDragEnd={() => {
setDraggingId(null);
setDragOverId(null);
}}
onDragEnd={() => setDraggingId(null)}
selected={selected}
onClick={() => navigate(href)}
sx={{ borderRadius: 1, mb: 0.5, cursor: "grab", ...navRowSx(selected) }}
sx={{
borderRadius: 1,
mb: 0.5,
cursor: "grab",
// The row being lifted fades a bit (its "real" position, distinct from the
// small custom ghost following the cursor -- see handleDragStart) and the
// row currently under the cursor gets an inset outline, so it's unambiguous
// which row a drop will land on instead of just a generic cursor change.
opacity: draggingId === entry.id ? 0.4 : 1,
...(dragOverId === entry.id && draggingId !== entry.id
? { outline: "2px solid", outlineColor: "primary.main", outlineOffset: "-2px" }
: {}),
...navRowSx(selected),
}}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<BookmarkIcon fontSize="small" />
+11 -11
View File
@@ -557,17 +557,6 @@
"uploading": "Uploading to {{target}}...",
"prompt": "Drop files or folders to upload to {{target}}"
},
"downloadZip": {
"loading": "Calculating…",
"summary_one": "{{count}} model (~{{size}})",
"summary_other": "{{count}} models (~{{size}})",
"summaryNoSize_one": "{{count}} model",
"summaryNoSize_other": "{{count}} models",
"summaryFailed": "Couldn't calculate the download size. Please try again.",
"empty": "No downloadable model files found.",
"downloading": "Downloading…",
"failed": "Failed to download the zip file. Please try again."
},
"errors": {
"loadPrintsFailed": "Failed to load prints. Please sign in again or refresh.",
"loadMoreFailed": "Failed to load more prints.",
@@ -1232,6 +1221,17 @@
"empty": "No models with this tag yet."
}
},
"downloadZip": {
"loading": "Calculating…",
"summary_one": "{{count}} model (~{{size}})",
"summary_other": "{{count}} models (~{{size}})",
"summaryNoSize_one": "{{count}} model",
"summaryNoSize_other": "{{count}} models",
"summaryFailed": "Couldn't calculate the download size. Please try again.",
"empty": "No downloadable model files found.",
"downloading": "Downloading…",
"failed": "Failed to download the zip file. Please try again."
},
"errors": {
"loadFailed": "Unable to load models.",
"loadCategoriesFailed": "Unable to load categories.",
+11 -11
View File
@@ -557,17 +557,6 @@
"uploading": "Įkeliama į {{target}}...",
"prompt": "Vilkite failus ar aplankus, kad įkeltumėte į {{target}}"
},
"downloadZip": {
"loading": "Skaičiuojama…",
"summary_one": "{{count}} modelis (~{{size}})",
"summary_other": "{{count}} modeliai (~{{size}})",
"summaryNoSize_one": "{{count}} modelis",
"summaryNoSize_other": "{{count}} modeliai",
"summaryFailed": "Nepavyko apskaičiuoti atsisiuntimo dydžio. Bandykite dar kartą.",
"empty": "Nerasta modelių failų atsisiuntimui.",
"downloading": "Atsisiunčiama…",
"failed": "Nepavyko atsisiųsti zip failo. Bandykite dar kartą."
},
"errors": {
"loadPrintsFailed": "Nepavyko įkelti spaudinių. Prisijunkite iš naujo arba atnaujinkite puslapį.",
"loadMoreFailed": "Nepavyko įkelti daugiau spaudinių.",
@@ -1233,6 +1222,17 @@
"empty": "Su šia žyma modelių kol kas nėra."
}
},
"downloadZip": {
"loading": "Skaičiuojama…",
"summary_one": "{{count}} modelis (~{{size}})",
"summary_other": "{{count}} modeliai (~{{size}})",
"summaryNoSize_one": "{{count}} modelis",
"summaryNoSize_other": "{{count}} modeliai",
"summaryFailed": "Nepavyko apskaičiuoti atsisiuntimo dydžio. Bandykite dar kartą.",
"empty": "Nerasta modelių failų atsisiuntimui.",
"downloading": "Atsisiunčiama…",
"failed": "Nepavyko atsisiųsti zip failo. Bandykite dar kartą."
},
"errors": {
"loadFailed": "Nepavyko įkelti modelių.",
"loadCategoriesFailed": "Nepavyko įkelti kategorijų.",