mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-24 08:22:10 -04:00
Enhance PathBar component with editing functionality
- Introduced editing mode in the PathBar component, allowing users to modify paths directly. - Added state management for editing, including handling input changes and keyboard events for submission and cancellation. - Updated the rendering logic to accommodate editing input, improving user interaction. - Refactored device icon retrieval to streamline the process and ensure correct icon display based on device state. - Adjusted width calculations for different states, enhancing the visual responsiveness of the PathBar.
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
RadioButtonIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { SdPath, LibraryDeviceInfo } from "@sd/ts-client";
|
||||
import { getDeviceIconBySlug, useLibraryMutation } from "@sd/ts-client";
|
||||
import { getDeviceIcon, useLibraryMutation } from "@sd/ts-client";
|
||||
import { sdPathToUri } from "../utils";
|
||||
import LaptopIcon from "@sd/assets/icons/Laptop.png";
|
||||
import { useNormalizedQuery } from "@sd/ts-client";
|
||||
@@ -250,6 +250,9 @@ function IndexIndicator({ path }: { path: SdPath }) {
|
||||
export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isShiftHeld, setIsShiftHeld] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
const [editingAsUri, setEditingAsUri] = useState(false);
|
||||
const { navigateToView } = useExplorer();
|
||||
const uri = sdPathToUri(path);
|
||||
const currentDir = getCurrentDirectoryName(path);
|
||||
@@ -264,7 +267,7 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
(d) => d.slug === deviceSlug,
|
||||
);
|
||||
return {
|
||||
icon: getDeviceIconBySlug(deviceSlug, devices),
|
||||
icon: device ? getDeviceIcon(device) : LaptopIcon,
|
||||
device,
|
||||
};
|
||||
}
|
||||
@@ -278,6 +281,68 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const enterEditMode = (initialValue: string, asUri: boolean) => {
|
||||
setIsEditing(true);
|
||||
setEditValue(initialValue);
|
||||
setEditingAsUri(asUri);
|
||||
};
|
||||
|
||||
const exitEditMode = () => {
|
||||
setIsEditing(false);
|
||||
setEditValue("");
|
||||
setEditingAsUri(false);
|
||||
};
|
||||
|
||||
const handleContainerClick = (e: React.MouseEvent) => {
|
||||
// Only enter edit mode if clicking the container itself, not buttons/segments
|
||||
if (e.target === e.currentTarget || (e.target as HTMLElement).tagName === "INPUT") {
|
||||
const isUriMode = showUri;
|
||||
const valueToEdit = isUriMode ? uri : ("Physical" in path ? path.Physical.path : uri);
|
||||
enterEditMode(valueToEdit, isUriMode);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
submitEdit();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
exitEditMode();
|
||||
}
|
||||
};
|
||||
|
||||
const submitEdit = () => {
|
||||
const trimmed = editValue.trim();
|
||||
if (!trimmed) {
|
||||
exitEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingAsUri) {
|
||||
// Try to parse as SdPath JSON
|
||||
const parsed = JSON.parse(trimmed) as SdPath;
|
||||
onNavigate(parsed);
|
||||
} else {
|
||||
// Parse as file path string
|
||||
if ("Physical" in path) {
|
||||
const newPath: SdPath = {
|
||||
Physical: {
|
||||
device_slug: path.Physical.device_slug,
|
||||
path: trimmed.startsWith("/") ? trimmed : `/${trimmed}`,
|
||||
},
|
||||
};
|
||||
onNavigate(newPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to parse path:", error);
|
||||
}
|
||||
|
||||
exitEditMode();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Shift") setIsShiftHeld(true);
|
||||
@@ -297,7 +362,7 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
|
||||
const showUri = isExpanded && isShiftHeld;
|
||||
|
||||
// Calculate widths for three states
|
||||
// Calculate widths for different states
|
||||
const collapsedWidth = currentDir.length * 8.5 + 70;
|
||||
const breadcrumbsWidth = Math.min(
|
||||
segments.reduce((sum, seg) => sum + seg.name.length * 6.5, 0) +
|
||||
@@ -306,29 +371,37 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
600,
|
||||
);
|
||||
const uriWidth = Math.min(uri.length * 7 + 70, 600);
|
||||
const editWidth = Math.max(200, Math.min(editValue.length * 7 + 70, 600));
|
||||
|
||||
const currentWidth = !isExpanded
|
||||
? collapsedWidth
|
||||
: showUri
|
||||
? uriWidth
|
||||
: breadcrumbsWidth;
|
||||
const currentWidth = isEditing
|
||||
? editWidth
|
||||
: !isExpanded
|
||||
? collapsedWidth
|
||||
: showUri
|
||||
? uriWidth
|
||||
: breadcrumbsWidth;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div
|
||||
animate={{ width: currentWidth }}
|
||||
transition={{ duration: 0.2, ease: [0.25, 1, 0.5, 1] }}
|
||||
onMouseEnter={() => setIsExpanded(true)}
|
||||
onMouseLeave={() => setIsExpanded(false)}
|
||||
onMouseEnter={() => !isEditing && setIsExpanded(true)}
|
||||
onMouseLeave={() => !isEditing && setIsExpanded(false)}
|
||||
onClick={handleContainerClick}
|
||||
className={clsx(
|
||||
"flex items-center gap-1.5 h-8 px-3 rounded-full",
|
||||
"backdrop-blur-xl border border-sidebar-line/30",
|
||||
"bg-sidebar-box/20 transition-colors",
|
||||
"focus-within:bg-sidebar-box/30 focus-within:border-sidebar-line/40",
|
||||
!isEditing && "cursor-text",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={handleDeviceClick}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeviceClick();
|
||||
}}
|
||||
disabled={!deviceInfo.device}
|
||||
title={
|
||||
deviceInfo.device
|
||||
@@ -349,7 +422,24 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
/>
|
||||
</button>
|
||||
|
||||
{showUri ? (
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleEditKeyDown}
|
||||
onBlur={exitEditMode}
|
||||
autoFocus
|
||||
className={clsx(
|
||||
"bg-transparent border-0 outline-none ring-0 flex-1 min-w-0",
|
||||
"text-xs font-medium text-sidebar-ink",
|
||||
"placeholder:text-sidebar-inkFaint",
|
||||
"focus:ring-0 focus:outline-none",
|
||||
editingAsUri && "font-mono",
|
||||
)}
|
||||
placeholder={editingAsUri ? "Enter SdPath JSON..." : "Enter path..."}
|
||||
/>
|
||||
) : showUri ? (
|
||||
<input
|
||||
type="text"
|
||||
value={uri}
|
||||
@@ -373,9 +463,10 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
className="flex items-center gap-1 flex-shrink-0"
|
||||
>
|
||||
<button
|
||||
onClick={() =>
|
||||
!isLast && onNavigate(segment.path)
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
!isLast && onNavigate(segment.path);
|
||||
}}
|
||||
disabled={isLast}
|
||||
className={clsx(
|
||||
"text-xs font-medium transition-colors whitespace-nowrap",
|
||||
@@ -386,7 +477,18 @@ export function PathBar({ path, devices, onNavigate }: PathBarProps) {
|
||||
>
|
||||
{segment.name}
|
||||
</button>
|
||||
{!isLast && <CaretRight size={12} />}
|
||||
{!isLast && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const valueToEdit = "Physical" in path ? path.Physical.path : uri;
|
||||
enterEditMode(valueToEdit, false);
|
||||
}}
|
||||
className="opacity-50 hover:opacity-100 transition-opacity cursor-text"
|
||||
>
|
||||
<CaretRight size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -141,7 +141,7 @@ function SyncButton() {
|
||||
/>
|
||||
}
|
||||
side="top"
|
||||
align="end"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-[380px] max-h-[520px] z-50 !p-0 !bg-app !rounded-xl"
|
||||
>
|
||||
@@ -247,7 +247,7 @@ function JobsButton({
|
||||
/>
|
||||
}
|
||||
side="top"
|
||||
align="end"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="w-[360px] max-h-[480px] z-50 !p-0 !bg-app !rounded-xl"
|
||||
>
|
||||
@@ -421,22 +421,24 @@ export function SpacesSidebar({ isPreviewActive = false }: SpacesSidebarProps) {
|
||||
</div>
|
||||
|
||||
{/* Sync Monitor, Job Manager, Customize & Settings (pinned to bottom) */}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<SyncButton />
|
||||
<JobsButton
|
||||
activeJobCount={activeJobCount}
|
||||
hasRunningJobs={hasRunningJobs}
|
||||
jobs={jobs}
|
||||
pause={pause}
|
||||
resume={resume}
|
||||
cancel={cancel}
|
||||
navigate={navigate}
|
||||
/>
|
||||
<TopBarButton
|
||||
icon={Palette}
|
||||
title="Customize"
|
||||
onClick={() => setCustomizePanelOpen(true)}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<SyncButton />
|
||||
<JobsButton
|
||||
activeJobCount={activeJobCount}
|
||||
hasRunningJobs={hasRunningJobs}
|
||||
jobs={jobs}
|
||||
pause={pause}
|
||||
resume={resume}
|
||||
cancel={cancel}
|
||||
navigate={navigate}
|
||||
/>
|
||||
<TopBarButton
|
||||
icon={Palette}
|
||||
title="Customize"
|
||||
onClick={() => setCustomizePanelOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<TopBarButton
|
||||
icon={GearSix}
|
||||
title="Settings"
|
||||
|
||||
Reference in New Issue
Block a user