diff --git a/__tests__/components/editor/developer/DeveloperEditorLayout.test.tsx b/__tests__/components/editor/developer/DeveloperEditorLayout.test.tsx
index 978f490c..1c41d669 100644
--- a/__tests__/components/editor/developer/DeveloperEditorLayout.test.tsx
+++ b/__tests__/components/editor/developer/DeveloperEditorLayout.test.tsx
@@ -2,6 +2,8 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import React from "react";
+import { useSelectionStore } from "@/lib/stores/selectionStore";
+
// Provide localStorage polyfill
vi.hoisted(() => {
if (!globalThis.localStorage || typeof globalThis.localStorage.setItem !== "function") {
@@ -1332,4 +1334,32 @@ describe("DeveloperEditorLayout", () => {
expect(navigateToNode).toHaveBeenCalledWith("http://example.org/ClassA");
});
});
+
+ describe("selection store cross-page contract", () => {
+ beforeEach(() => useSelectionStore.getState().clear());
+
+ it("populates the selection store from selectedIri on mount", () => {
+ render(
+ ,
+ );
+ expect(useSelectionStore.getState().iri).toBe("http://example.org/Foo");
+ expect(useSelectionStore.getState().type).toBe("class");
+ });
+
+ it("preserves the selection store on unmount so side-page Back-to-project links can read it", () => {
+ // Regression: editor used to clear the store on unmount, racing with
+ // side-page navigation — by the time settings/PRs/etc rendered, the
+ // store was empty and useProjectHomeHref dropped the selection.
+ const { unmount } = render(
+ ,
+ );
+ unmount();
+ expect(useSelectionStore.getState().iri).toBe("http://example.org/Foo");
+ expect(useSelectionStore.getState().type).toBe("class");
+ });
+ });
});
diff --git a/__tests__/components/editor/standard/StandardEditorLayout.test.tsx b/__tests__/components/editor/standard/StandardEditorLayout.test.tsx
index c15c3ecc..72c10e2b 100644
--- a/__tests__/components/editor/standard/StandardEditorLayout.test.tsx
+++ b/__tests__/components/editor/standard/StandardEditorLayout.test.tsx
@@ -2,6 +2,8 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
import { act, render, screen, fireEvent } from "@testing-library/react";
import React from "react";
+import { useSelectionStore } from "@/lib/stores/selectionStore";
+
// Provide localStorage polyfill
vi.hoisted(() => {
if (!globalThis.localStorage || typeof globalThis.localStorage.setItem !== "function") {
@@ -1040,4 +1042,32 @@ describe("StandardEditorLayout", () => {
expect(navigateToNode).toHaveBeenCalledWith("http://example.org/Unknown");
});
});
+
+ describe("selection store cross-page contract", () => {
+ beforeEach(() => useSelectionStore.getState().clear());
+
+ it("populates the selection store from selectedIri on mount", () => {
+ render(
+ ,
+ );
+ expect(useSelectionStore.getState().iri).toBe("http://example.org/Foo");
+ expect(useSelectionStore.getState().type).toBe("class");
+ });
+
+ it("preserves the selection store on unmount so side-page Back-to-project links can read it", () => {
+ // Regression: editor used to clear the store on unmount, racing with
+ // side-page navigation — by the time settings/PRs/etc rendered, the
+ // store was empty and useProjectHomeHref dropped the selection.
+ const { unmount } = render(
+ ,
+ );
+ unmount();
+ expect(useSelectionStore.getState().iri).toBe("http://example.org/Foo");
+ expect(useSelectionStore.getState().type).toBe("class");
+ });
+ });
});
diff --git a/__tests__/lib/hooks/useProjectHomeHref.test.ts b/__tests__/lib/hooks/useProjectHomeHref.test.ts
new file mode 100644
index 00000000..66e8e03c
--- /dev/null
+++ b/__tests__/lib/hooks/useProjectHomeHref.test.ts
@@ -0,0 +1,150 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { renderHook } from "@testing-library/react";
+
+// editorModeStore subscribes to matchMedia at module load — provide a stub
+// before the hook (and therefore the store) are imported.
+vi.hoisted(() => {
+ (globalThis as Record).matchMedia = vi.fn().mockReturnValue({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ });
+
+ // Zustand persist middleware needs a working localStorage at module-load.
+ if (
+ !globalThis.localStorage ||
+ typeof globalThis.localStorage.setItem !== "function"
+ ) {
+ const store = new Map();
+ (globalThis as Record).localStorage = {
+ getItem: (key: string) => store.get(key) ?? null,
+ setItem: (key: string, value: string) => store.set(key, value),
+ removeItem: (key: string) => store.delete(key),
+ clear: () => store.clear(),
+ get length() {
+ return store.size;
+ },
+ key: (index: number) => [...store.keys()][index] ?? null,
+ };
+ }
+});
+
+import { useProjectHomeHref } from "@/lib/hooks/useProjectHomeHref";
+import { useSelectionStore } from "@/lib/stores/selectionStore";
+import { useEditorModeStore } from "@/lib/stores/editorModeStore";
+
+// next-auth: simple session-with-token mock; individual tests can override.
+let mockSession: { accessToken?: string } | null = { accessToken: "tok" };
+vi.mock("next-auth/react", () => ({
+ useSession: () => ({ data: mockSession }),
+}));
+
+// next/navigation: only useSearchParams is consumed by the hook.
+let mockSearch = "";
+vi.mock("next/navigation", () => ({
+ useSearchParams: () => new URLSearchParams(mockSearch),
+}));
+
+// useProject: mocked so we can dial canSuggest via derivePermissions.
+const mockUseProject = vi.fn();
+const mockDerivePermissions = vi.fn();
+vi.mock("@/lib/hooks/useProject", () => ({
+ useProject: (...args: unknown[]) => mockUseProject(...args),
+ derivePermissions: (...args: unknown[]) => mockDerivePermissions(...args),
+}));
+
+const ENCODED_PERSON = encodeURIComponent("http://example.org/Person");
+const ENCODED_HAS_NAME = encodeURIComponent("http://example.org/hasName");
+const ENCODED_ALICE = encodeURIComponent("http://example.org/alice");
+
+describe("useProjectHomeHref", () => {
+ beforeEach(() => {
+ mockSession = { accessToken: "tok" };
+ mockSearch = "";
+ useSelectionStore.getState().clear();
+ useEditorModeStore.setState({ preferEditMode: false });
+ mockUseProject.mockReturnValue({ project: { id: "proj-1" } });
+ // Default: user can suggest (covers most assertions).
+ mockDerivePermissions.mockReturnValue({ canSuggest: true });
+ });
+
+ it("returns the bare viewer URL when no selection is set", () => {
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe("/projects/proj-1");
+ });
+
+ it("returns the editor URL when preferEditMode is on and user can suggest", () => {
+ useEditorModeStore.setState({ preferEditMode: true });
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe("/projects/proj-1/editor");
+ });
+
+ it("routes prefer-edit-mode users to the viewer when they cannot suggest", () => {
+ useEditorModeStore.setState({ preferEditMode: true });
+ mockDerivePermissions.mockReturnValue({ canSuggest: false });
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe("/projects/proj-1");
+ });
+
+ it("routes back to the editor when the store records mode='editor', even with preferEditMode off", () => {
+ // Regression: preferEditMode false used to force viewer-routing, so a user
+ // who switched to the editor mid-session via the switcher would lose the
+ // mode on Back-to-project.
+ useEditorModeStore.setState({ preferEditMode: false });
+ useSelectionStore.getState().setMode("editor");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe("/projects/proj-1/editor");
+ });
+
+ it("routes back to the viewer when the store records mode='viewer', even with preferEditMode on", () => {
+ useEditorModeStore.setState({ preferEditMode: true });
+ useSelectionStore.getState().setMode("viewer");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe("/projects/proj-1");
+ });
+
+ it("permission-gates editor routing even when the store says mode='editor'", () => {
+ useSelectionStore.getState().setMode("editor");
+ mockDerivePermissions.mockReturnValue({ canSuggest: false });
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe("/projects/proj-1");
+ });
+
+ it("appends a class IRI selection from the store", () => {
+ useSelectionStore.getState().setSelection("http://example.org/Person", "class");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe(`/projects/proj-1?classIri=${ENCODED_PERSON}`);
+ });
+
+ it("appends a property IRI selection from the store", () => {
+ useSelectionStore.getState().setSelection("http://example.org/hasName", "property");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe(`/projects/proj-1?propertyIri=${ENCODED_HAS_NAME}`);
+ });
+
+ it("appends an individual IRI selection from the store", () => {
+ useSelectionStore.getState().setSelection("http://example.org/alice", "individual");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe(`/projects/proj-1?individualIri=${ENCODED_ALICE}`);
+ });
+
+ it("appends selection to the editor URL when preferEditMode is on", () => {
+ useEditorModeStore.setState({ preferEditMode: true });
+ useSelectionStore.getState().setSelection("http://example.org/Person", "class");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe(`/projects/proj-1/editor?classIri=${ENCODED_PERSON}`);
+ });
+
+ it("falls back to URL params when the store is empty", () => {
+ mockSearch = `individualIri=${ENCODED_ALICE}`;
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe(`/projects/proj-1?individualIri=${ENCODED_ALICE}`);
+ });
+
+ it("prefers the store over URL params when both are set (store wins)", () => {
+ mockSearch = `classIri=${encodeURIComponent("http://example.org/Stale")}`;
+ useSelectionStore.getState().setSelection("http://example.org/hasName", "property");
+ const { result } = renderHook(() => useProjectHomeHref("proj-1"));
+ expect(result.current).toBe(`/projects/proj-1?propertyIri=${ENCODED_HAS_NAME}`);
+ });
+});
diff --git a/__tests__/lib/stores/selectionStore.test.ts b/__tests__/lib/stores/selectionStore.test.ts
index 41022211..244ab128 100644
--- a/__tests__/lib/stores/selectionStore.test.ts
+++ b/__tests__/lib/stores/selectionStore.test.ts
@@ -33,11 +33,13 @@ describe("useSelectionStore", () => {
});
});
- it("clear resets back to null/null", () => {
+ it("clear resets selection and mode to null", () => {
useSelectionStore.getState().setSelection("ex:Person", "class");
+ useSelectionStore.getState().setMode("editor");
useSelectionStore.getState().clear();
expect(useSelectionStore.getState().iri).toBeNull();
expect(useSelectionStore.getState().type).toBeNull();
+ expect(useSelectionStore.getState().mode).toBeNull();
});
it("supports null iri with null type for an empty active tab", () => {
@@ -45,4 +47,18 @@ describe("useSelectionStore", () => {
expect(useSelectionStore.getState().iri).toBeNull();
expect(useSelectionStore.getState().type).toBeNull();
});
+
+ it("starts with mode null", () => {
+ expect(useSelectionStore.getState().mode).toBeNull();
+ });
+
+ it("setMode records viewer / editor independently of selection", () => {
+ useSelectionStore.getState().setMode("editor");
+ expect(useSelectionStore.getState().mode).toBe("editor");
+ // Selection untouched
+ expect(useSelectionStore.getState().iri).toBeNull();
+
+ useSelectionStore.getState().setMode("viewer");
+ expect(useSelectionStore.getState().mode).toBe("viewer");
+ });
});
diff --git a/app/projects/[id]/editor/page.tsx b/app/projects/[id]/editor/page.tsx
index b8ae215a..186e3773 100644
--- a/app/projects/[id]/editor/page.tsx
+++ b/app/projects/[id]/editor/page.tsx
@@ -23,6 +23,7 @@ import { BranchProvider, branchQueryKeys } from "@/lib/context/BranchContext";
import { useProjectViewer } from "@/lib/hooks/useProjectViewer";
import { ConnectionStatus } from "@/components/ui/ConnectionStatus";
import { useEditorModeStore } from "@/lib/stores/editorModeStore";
+import { useSelectionStore } from "@/lib/stores/selectionStore";
import { revisionsApi } from "@/lib/api/revisions";
import { projectOntologyApi, type ClassUpdatePayload } from "@/lib/api/client";
import { getLocalName } from "@/lib/utils";
@@ -71,6 +72,14 @@ export default function EditorPage() {
const editorMode = useEditorModeStore((s) => s.editorMode);
+ // Mark this surface as the user's most recent project view so side-page
+ // Back-to-project links route them back to the editor — regardless of
+ // whether their global preferEditMode preference says viewer or editor.
+ const setProjectViewMode = useSelectionStore((s) => s.setMode);
+ useEffect(() => {
+ setProjectViewMode("editor");
+ }, [setProjectViewMode]);
+
// Branch state
const queryClient = useQueryClient();
const [activeBranch, setActiveBranch] = useState(undefined);
@@ -693,8 +702,14 @@ export default function EditorPage() {
}
setActiveBranch(branchName);
resetSourceState();
- router.replace(`${pathname}?branch=${encodeURIComponent(branchName)}`);
- }, [pathname, router, suggestionSession, resetSourceState]);
+ // Merge into existing search params instead of replacing — BranchSelector
+ // fires this on mount, and replacing would wipe ?classIri= / ?propertyIri=
+ // / ?individualIri= / ?resumeSession= that the page also depends on.
+ const next = new URLSearchParams(searchParamsString);
+ next.set("branch", branchName);
+ const qs = next.toString();
+ router.replace(qs ? `${pathname}?${qs}` : pathname);
+ }, [pathname, router, suggestionSession, resetSourceState, searchParamsString]);
// --- Keyboard shortcuts ---
const keyboardShortcuts = useMemo((): ShortcutDefinition[] => [
diff --git a/app/projects/[id]/page.tsx b/app/projects/[id]/page.tsx
index b364e2b6..45dd58a2 100644
--- a/app/projects/[id]/page.tsx
+++ b/app/projects/[id]/page.tsx
@@ -16,6 +16,7 @@ import { StandardEditorLayout } from "@/components/editor/standard/StandardEdito
import { BranchProvider, useBranch } from "@/lib/context/BranchContext";
import { useProjectViewer } from "@/lib/hooks/useProjectViewer";
import { useEditorModeStore } from "@/lib/stores/editorModeStore";
+import { useSelectionStore } from "@/lib/stores/selectionStore";
import { useToast } from "@/lib/context/ToastContext";
import { useProject, derivePermissions } from "@/lib/hooks/useProject";
import type { OntologySourceEditorRef } from "@/components/editor/OntologySourceEditor";
@@ -170,6 +171,14 @@ function ViewerContent({
);
const editorMode = useEditorModeStore((s) => s.editorMode);
+ // Mark this surface as the user's most recent project view so side-page
+ // Back-to-project links route them back to the viewer — regardless of
+ // whether their global preferEditMode preference says viewer or editor.
+ const setProjectViewMode = useSelectionStore((s) => s.setMode);
+ useEffect(() => {
+ setProjectViewMode("viewer");
+ }, [setProjectViewMode]);
+
// Use the default branch from the BranchProvider context
const { defaultBranch, isLoading: isBranchLoading } = useBranch();
const resolvedBranch = isBranchLoading ? undefined : defaultBranch;
diff --git a/components/editor/developer/DeveloperEditorLayout.tsx b/components/editor/developer/DeveloperEditorLayout.tsx
index e4502a61..d6638d7b 100644
--- a/components/editor/developer/DeveloperEditorLayout.tsx
+++ b/components/editor/developer/DeveloperEditorLayout.tsx
@@ -287,10 +287,16 @@ export function DeveloperEditorLayout(props: DeveloperEditorLayoutProps) {
}, [entityNavigationRef, navToNode, setScrollIri]);
// Mirror the active-tab selection into the shared store so cross-page chrome
- // (e.g. the Viewer/Editor switcher) can synthesize the right ?Iri= key
- // without having to know about local-state plumbing here.
+ // (e.g. the Viewer/Editor switcher, the side-page Back-to-project link via
+ // useProjectHomeHref) can synthesize the right ?Iri= key without having
+ // to know about local-state plumbing here.
+ //
+ // No unmount cleanup: an unmount-clear races with side-page navigation —
+ // the editor unmounts before settings/PRs/etc render, so reading the store
+ // there returns null and the back-link drops the selection. The next editor
+ // mount (same or different project) overwrites the store from its own URL
+ // params, so stale values are short-lived and harmless.
const setSelection = useSelectionStore((s) => s.setSelection);
- const clearSelection = useSelectionStore((s) => s.clear);
useEffect(() => {
if (activeTab === "classes") {
setSelection(selectedIri ?? null, selectedIri ? "class" : null);
@@ -300,7 +306,6 @@ export function DeveloperEditorLayout(props: DeveloperEditorLayoutProps) {
setSelection(selectedIndividualIri ?? null, selectedIndividualIri ? "individual" : null);
}
}, [activeTab, selectedIri, selectedPropertyIri, selectedIndividualIri, setSelection]);
- useEffect(() => () => clearSelection(), [clearSelection]);
// Shared search state
const {
diff --git a/components/editor/standard/StandardEditorLayout.tsx b/components/editor/standard/StandardEditorLayout.tsx
index e7a81e94..7bdeca78 100644
--- a/components/editor/standard/StandardEditorLayout.tsx
+++ b/components/editor/standard/StandardEditorLayout.tsx
@@ -252,10 +252,16 @@ export function StandardEditorLayout(props: StandardEditorLayoutProps) {
}, [entityNavigationRef, navToNode]);
// Mirror the active-tab selection into the shared store so cross-page chrome
- // (e.g. the Viewer/Editor switcher) can synthesize the right ?Iri= key
- // without having to know about local-state plumbing here.
+ // (e.g. the Viewer/Editor switcher, the side-page Back-to-project link via
+ // useProjectHomeHref) can synthesize the right ?Iri= key without having
+ // to know about local-state plumbing here.
+ //
+ // No unmount cleanup: an unmount-clear races with side-page navigation —
+ // the editor unmounts before settings/PRs/etc render, so reading the store
+ // there returns null and the back-link drops the selection. The next editor
+ // mount (same or different project) overwrites the store from its own URL
+ // params, so stale values are short-lived and harmless.
const setSelection = useSelectionStore((s) => s.setSelection);
- const clearSelection = useSelectionStore((s) => s.clear);
useEffect(() => {
if (activeTab === "classes") {
setSelection(selectedIri ?? null, selectedIri ? "class" : null);
@@ -265,7 +271,6 @@ export function StandardEditorLayout(props: StandardEditorLayoutProps) {
setSelection(selectedIndividualIri ?? null, selectedIndividualIri ? "individual" : null);
}
}, [activeTab, selectedIri, selectedPropertyIri, selectedIndividualIri, setSelection]);
- useEffect(() => () => clearSelection(), [clearSelection]);
// Shared search state
const {
diff --git a/lib/hooks/useProjectHomeHref.ts b/lib/hooks/useProjectHomeHref.ts
index 307fdcdc..886d4238 100644
--- a/lib/hooks/useProjectHomeHref.ts
+++ b/lib/hooks/useProjectHomeHref.ts
@@ -1,22 +1,32 @@
"use client";
import { useSession } from "next-auth/react";
+import { useSearchParams } from "next/navigation";
import { derivePermissions, useProject } from "@/lib/hooks/useProject";
import { useEditorModeStore } from "@/lib/stores/editorModeStore";
+import { useSelectionStore } from "@/lib/stores/selectionStore";
+import {
+ buildSelectionQuery,
+ readSelectionFromSearchParams,
+} from "@/lib/utils/selectionUrl";
/**
- * Resolve the URL that should land the user "back at the project" — either
- * the read-only viewer or the editor, depending on the user's
- * preferEditMode preference and their permissions on this project.
+ * Resolve the URL that should land the user "back at the project" — the
+ * viewer or the editor, carrying the active entity selection (?classIri= /
+ * ?propertyIri= / ?individualIri=) so cross-page round-trips don't lose the
+ * user's place.
*
- * Used by every "Back to project" link in side pages (settings, PRs,
- * analytics, suggestions, dashboard) so that a user who has prefer-edit-mode
- * enabled and the right permissions returns to the editor — not always to
- * the viewer like the previous hard-coded `/projects/${projectId}` href did.
+ * Mode resolution, in order:
+ * 1. The `mode` field in useSelectionStore — set whenever the user is on
+ * the viewer or editor page. This makes Back-to-project semantically
+ * "go back where I just was," matching user expectation regardless of
+ * whether the user explicitly switched mode mid-session.
+ * 2. Fallback to the global preferEditMode preference (when the side page
+ * is the user's first project surface, e.g. via a deep link).
*
- * Read-only viewer-role users and unauthenticated visitors always get the
- * viewer URL regardless of preference, so the affordance can never land
- * someone where they have no rights.
+ * Permission gate: editor mode requires `canSuggest` — read-only viewer-role
+ * users and unauthenticated visitors are always routed to the viewer so the
+ * affordance can never land someone where they have no rights.
*/
export function useProjectHomeHref(projectId: string): string {
const { data: session } = useSession();
@@ -24,7 +34,22 @@ export function useProjectHomeHref(projectId: string): string {
const { canSuggest } = derivePermissions(project, session?.accessToken);
const preferEditMode = useEditorModeStore((s) => s.preferEditMode);
- return preferEditMode && canSuggest
- ? `/projects/${projectId}/editor`
- : `/projects/${projectId}`;
+ // Mirror ViewerEditorSwitcher: prefer the in-memory selection store
+ // (populated by the viewer/editor the user came from), fall back to URL
+ // params for first render or hard-deep-link cases.
+ const searchParams = useSearchParams();
+ const storeIri = useSelectionStore((s) => s.iri);
+ const storeType = useSelectionStore((s) => s.type);
+ const storeMode = useSelectionStore((s) => s.mode);
+ const selection =
+ storeIri && storeType
+ ? { iri: storeIri, type: storeType }
+ : readSelectionFromSearchParams(searchParams);
+
+ const wantsEditor = storeMode ? storeMode === "editor" : preferEditMode;
+ const base =
+ wantsEditor && canSuggest
+ ? `/projects/${projectId}/editor`
+ : `/projects/${projectId}`;
+ return `${base}${buildSelectionQuery(selection)}`;
}
diff --git a/lib/stores/selectionStore.ts b/lib/stores/selectionStore.ts
index cd6fec3f..e96a9551 100644
--- a/lib/stores/selectionStore.ts
+++ b/lib/stores/selectionStore.ts
@@ -2,23 +2,37 @@ import { create } from "zustand";
import type { SelectableEntityType } from "@/lib/utils/selectionUrl";
+/**
+ * Which project surface the user is most recently on. Used by side-page
+ * Back-to-project links to route the user back where they came from instead
+ * of forcing them through the global preferEditMode preference.
+ */
+export type ProjectViewMode = "viewer" | "editor";
+
interface SelectionState {
/** IRI of the entity the user is currently focused on (whichever tab is active). */
iri: string | null;
/** Entity type that resolves the punning ambiguity for {@link iri}. */
type: SelectableEntityType | null;
+ /** Most recent viewer/editor surface the user was on, or null on cold load. */
+ mode: ProjectViewMode | null;
setSelection: (iri: string | null, type: SelectableEntityType | null) => void;
+ setMode: (mode: ProjectViewMode) => void;
clear: () => void;
}
/**
- * In-session active-selection state shared between the entity-tab layouts and
- * cross-page chrome (e.g. the Viewer/Editor switcher). Not persisted — on a
- * full page reload, the URL search params drive initial state.
+ * In-session active-selection + viewer/editor mode state shared between
+ * project pages and cross-page chrome (Viewer/Editor switcher, side-page
+ * Back-to-project links via useProjectHomeHref). Not persisted — on a full
+ * page reload, the URL search params drive selection and the preferEditMode
+ * preference drives mode.
*/
export const useSelectionStore = create()((set) => ({
iri: null,
type: null,
+ mode: null,
setSelection: (iri, type) => set({ iri, type }),
- clear: () => set({ iri: null, type: null }),
+ setMode: (mode) => set({ mode }),
+ clear: () => set({ iri: null, type: null, mode: null }),
}));