mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 06:09:56 -05:00
fix: few minor ui issues
This commit is contained in:
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" />
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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ų.",
|
||||
|
||||
Reference in new issue
Block a user