fix: stabilize sidebar utility navigation
This commit is contained in:
1 parent
f065b8ef94
commit
47644ff73e
2 files changed
+66
-52
No files matched your search
@@ -72,7 +72,32 @@ const SidebarNavButton = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
const SidebarTrashNavButton = ({
|
||||
const SidebarShortcutButton = ({
|
||||
active = false,
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
active?: boolean;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}) => (
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-10 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-md px-1.5 text-xs font-medium transition-colors duration-200",
|
||||
active ? "bg-slate-100 text-slate-900" : "text-slate-600 hover:bg-slate-50 hover:text-slate-950"
|
||||
)}
|
||||
type="button"
|
||||
aria-current={active ? "page" : undefined}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center">{icon}</span>
|
||||
<span className="min-w-0 truncate">{label}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
const SidebarTrashShortcut = ({
|
||||
active = false,
|
||||
onOpenTrash,
|
||||
onEmptyTrash,
|
||||
@@ -84,37 +109,26 @@ const SidebarTrashNavButton = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="group relative">
|
||||
<button
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center gap-3 rounded-md px-3 pr-16 text-left text-sm font-medium leading-none transition-all duration-200",
|
||||
active ? "bg-slate-100 text-slate-950" : "text-slate-700 hover:bg-slate-50 hover:text-slate-950"
|
||||
)}
|
||||
type="button"
|
||||
aria-current={active ? "page" : undefined}
|
||||
onClick={onOpenTrash}
|
||||
>
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{t("notebookPane.trash")}</span>
|
||||
</button>
|
||||
<button
|
||||
className="pointer-events-none absolute right-1.5 top-1/2 flex h-7 -translate-y-1/2 items-center gap-1 rounded-md px-2 text-xs font-medium text-rose-700 opacity-0 transition-all duration-200 hover:bg-rose-50 focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/70 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100"
|
||||
type="button"
|
||||
title={t("notebookPane.emptyTrash")}
|
||||
aria-label={t("notebookPane.emptyTrash")}
|
||||
onClick={onEmptyTrash}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
{t("notebookPane.empty")}
|
||||
</button>
|
||||
<div className="group relative min-w-0">
|
||||
<SidebarShortcutButton active={active} icon={<Trash2 className="h-4 w-4" />} label={t("notebookPane.trash")} onClick={onOpenTrash} />
|
||||
{!active && (
|
||||
<div className="pointer-events-none absolute left-1/2 top-full z-20 w-max -translate-x-1/2 pt-1 opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<button
|
||||
className="flex h-8 items-center gap-1.5 rounded-md border border-rose-200 bg-white px-2 text-xs font-medium text-rose-700 shadow-lg shadow-slate-900/10 transition-colors hover:bg-rose-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400/70"
|
||||
type="button"
|
||||
onClick={onEmptyTrash}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
{t("notebookPane.emptyTrash")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarSectionLabel = ({ icon, label }: { icon: ReactNode; label: string }) => (
|
||||
<div className="flex h-9 items-center gap-3 px-3 text-sm font-medium leading-none text-slate-600">
|
||||
<div className="flex h-9 items-center gap-3 px-3 text-xs font-medium leading-none tracking-wide text-slate-500">
|
||||
<span className="flex h-4 w-4 shrink-0 items-center justify-center">{icon}</span>
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
</div>
|
||||
@@ -344,22 +358,27 @@ export const NotebookPane = ({
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<header className="flex h-[calc(4rem+env(safe-area-inset-top))] shrink-0 items-end justify-between border-b border-slate-200 px-4 pb-3 pt-[env(safe-area-inset-top)] lg:h-16 lg:items-center lg:pb-0 lg:pt-0">
|
||||
<header className="flex h-[calc(4rem+env(safe-area-inset-top))] shrink-0 items-end justify-between border-b border-slate-200 px-4 pb-3 pt-[env(safe-area-inset-top)] lg:hidden">
|
||||
<div>
|
||||
<div className="text-base font-semibold tracking-normal lg:hidden">{t("notebookPane.notebooks")}</div>
|
||||
<div className="hidden text-base font-semibold tracking-normal lg:block">EdgeEver</div>
|
||||
<div className="text-base font-semibold tracking-normal">{t("notebookPane.notebooks")}</div>
|
||||
<div className="text-xs text-slate-500">{user?.username ?? t("notebookPane.workspaceFallback")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button className="lg:hidden" size="icon" variant="ghost" title={t("notebookPane.backToList")} aria-label={t("notebookPane.backToList")} onClick={onBackToList}>
|
||||
<Button size="icon" variant="ghost" title={t("notebookPane.backToList")} aria-label={t("notebookPane.backToList")} onClick={onBackToList}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button className="lg:hidden" size="icon" variant="ghost" title={t("notebookPane.newNotebook")} aria-label={t("notebookPane.newNotebook")} onClick={() => onCreateNotebook(null)}>
|
||||
<Button size="icon" variant="ghost" title={t("notebookPane.newNotebook")} aria-label={t("notebookPane.newNotebook")} onClick={() => onCreateNotebook(null)}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav className="grid shrink-0 grid-cols-3 gap-1 border-b border-slate-100 px-3 py-2" aria-label={t("notebookPane.secondaryEntries")}>
|
||||
<SidebarShortcutButton icon={<Tags className="h-4 w-4" />} label={t("mobileSheets.tags")} onClick={onOpenTags} />
|
||||
<SidebarShortcutButton icon={<Archive className="h-4 w-4" />} label={t("mobileSheets.assets")} onClick={onOpenAssets} />
|
||||
<SidebarTrashShortcut active={view === "trash"} onOpenTrash={onOpenTrash} onEmptyTrash={onEmptyTrash} />
|
||||
</nav>
|
||||
|
||||
<div
|
||||
ref={notebookScrollRef}
|
||||
className="flex-1 overflow-y-auto px-3 py-4"
|
||||
@@ -458,11 +477,6 @@ export const NotebookPane = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<nav className="space-y-1 border-t border-slate-100 pt-3" aria-label={t("notebookPane.secondaryEntries")}>
|
||||
<SidebarNavButton icon={<Tags className="h-4 w-4" />} label={t("mobileSheets.tags")} onClick={onOpenTags} />
|
||||
<SidebarNavButton icon={<Archive className="h-4 w-4" />} label={t("mobileSheets.assets")} onClick={onOpenAssets} />
|
||||
<SidebarTrashNavButton active={view === "trash"} onOpenTrash={onOpenTrash} onEmptyTrash={onEmptyTrash} />
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<footer className="border-t border-slate-200 bg-white/80 px-3 pb-[max(0.5rem,env(safe-area-inset-bottom))] pt-2 backdrop-blur-sm">
|
||||
|
||||
@@ -108,6 +108,7 @@ const TagsDialog = lazy(() => import("./dialogs/TagsDialog").then((module) => ({
|
||||
const TemplatesDialog = lazy(() => import("./dialogs/TemplatesDialog").then((module) => ({ default: module.TemplatesDialog })));
|
||||
|
||||
const SETTINGS_PATH = "/settings";
|
||||
const TRASH_VIEW_SEARCH = "?view=trash";
|
||||
const getMobileEditorReturnMemoId = (search: string) => new URLSearchParams(search).get(MOBILE_EDITOR_RETURN_PARAM);
|
||||
const emptySyncQueueSummary = (): SyncQueueSummary => ({
|
||||
total: 0,
|
||||
@@ -739,8 +740,9 @@ export const WorkspaceApp = ({
|
||||
const navigate = useNavigate();
|
||||
const isInitialSettingsRoute = location.pathname === SETTINGS_PATH;
|
||||
const isInitialMobileEditorReturn = Boolean(getMobileEditorReturnMemoId(location.search));
|
||||
const isTrashRoute = location.pathname === "/" && location.search === TRASH_VIEW_SEARCH;
|
||||
const [activePane, setActivePane] = useState<Pane>(() => (isInitialSettingsRoute && !isInitialMobileEditorReturn ? "editor" : "memos"));
|
||||
const [memoView, setMemoView] = useState<MemoView>("notebook");
|
||||
const [memoView, setMemoView] = useState<MemoView>(() => (isTrashRoute ? "trash" : "notebook"));
|
||||
const [selectedNotebookId, setSelectedNotebookId] = useState<string | null>(null);
|
||||
const [selectedMemoId, setSelectedMemoId] = useState<string | null>(null);
|
||||
const [createdMemoEditId, setCreatedMemoEditId] = useState<string | null>(null);
|
||||
@@ -790,11 +792,17 @@ export const WorkspaceApp = ({
|
||||
const [desktopActionsOpen, setDesktopActionsOpen] = useState(false);
|
||||
|
||||
const navigateWorkspaceHome = () => {
|
||||
if (location.pathname !== "/") {
|
||||
if (location.pathname !== "/" || location.search) {
|
||||
navigate("/");
|
||||
}
|
||||
};
|
||||
|
||||
const navigateWorkspaceTrash = () => {
|
||||
if (location.pathname !== "/" || location.search !== TRASH_VIEW_SEARCH) {
|
||||
navigate(`/${TRASH_VIEW_SEARCH}`);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateWorkspaceSettings = () => {
|
||||
if (location.pathname !== SETTINGS_PATH) {
|
||||
navigate(SETTINGS_PATH);
|
||||
@@ -1016,9 +1024,10 @@ export const WorkspaceApp = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setMemoView(isTrashRoute ? "trash" : "notebook");
|
||||
setRightView("editor");
|
||||
setMobileBottomNavActive("home");
|
||||
}, [location.pathname]);
|
||||
}, [isTrashRoute, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
@@ -2346,16 +2355,7 @@ export const WorkspaceApp = ({
|
||||
onDeleteNotebook={handleDeleteNotebook}
|
||||
onMoveNotebook={handleMoveNotebook}
|
||||
onMoveMemos={handleMoveDraggedMemos}
|
||||
onBackToList={() => {
|
||||
navigateWorkspaceHome();
|
||||
if (memoView === "trash") {
|
||||
setMemoView("notebook");
|
||||
}
|
||||
setSelectedNotebookId(null);
|
||||
clearMemoSelection();
|
||||
setRightView("editor");
|
||||
setActivePane("memos");
|
||||
}}
|
||||
onBackToList={handleSelectAllMemos}
|
||||
onLogout={onLogout}
|
||||
isLoggingOut={isLoggingOut}
|
||||
imageCompressionEnabled={imageCompressionEnabled}
|
||||
@@ -2368,7 +2368,7 @@ export const WorkspaceApp = ({
|
||||
onOpenTags={handleOpenTags}
|
||||
onOpenSettings={handleOpenSettings}
|
||||
onOpenTrash={() => {
|
||||
navigateWorkspaceHome();
|
||||
navigateWorkspaceTrash();
|
||||
setMemoView("trash");
|
||||
setSelectedNotebookId(null);
|
||||
setMobileBottomNavActive("home");
|
||||
@@ -2434,7 +2434,7 @@ export const WorkspaceApp = ({
|
||||
isSyncingMemos={isManualMemoSyncing || isSyncingQueuedChanges || isPullRefreshing || memosQuery.isRefetching}
|
||||
canSyncMemos={isOnline}
|
||||
onOpenTrash={() => {
|
||||
navigateWorkspaceHome();
|
||||
navigateWorkspaceTrash();
|
||||
setMemoView("trash");
|
||||
setSelectedNotebookId(null);
|
||||
setMobileBottomNavActive("home");
|
||||
|
||||
Reference in new issue
Block a user