diff --git a/web/src/objects/objects-page.tsx b/web/src/objects/objects-page.tsx
index d3f490c..5894ec6 100644
--- a/web/src/objects/objects-page.tsx
+++ b/web/src/objects/objects-page.tsx
@@ -6,6 +6,7 @@ import { X } from "lucide-react";
import { ObjectsTable } from "./objects-table";
import { useMediaQuery } from "../lib/use-media-query";
import { useDocumentTitle } from "../lib/use-document-title";
+import { useBreadcrumb } from "../shell/use-breadcrumb";
import { PageTitle } from "@/components/ui/page-title";
const ObjectDetailDrawer = lazy(() =>
@@ -26,6 +27,7 @@ export function ObjectsPage() {
const isWide = useMediaQuery("(min-width: 1024px)");
useDocumentTitle(t("nav.objects"));
+ useBreadcrumb([{ label: t("nav.objects") }]);
const closeDetail = () => navigate(`/objects?${searchParams}`);
diff --git a/web/src/shell/app-shell.tsx b/web/src/shell/app-shell.tsx
index d7ff942..9c06929 100644
--- a/web/src/shell/app-shell.tsx
+++ b/web/src/shell/app-shell.tsx
@@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button";
import { LangSwitch } from "./lang-switch";
import { ThemeSwitch } from "./theme-switch";
import { Sidebar } from "./sidebar";
+import { BreadcrumbProvider } from "./breadcrumb-provider";
+import { Breadcrumb } from "./breadcrumb";
export function AppShell() {
const { t } = useTranslation();
@@ -20,19 +22,21 @@ export function AppShell() {
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/web/src/shell/breadcrumb-context.ts b/web/src/shell/breadcrumb-context.ts
new file mode 100644
index 0000000..d1efe51
--- /dev/null
+++ b/web/src/shell/breadcrumb-context.ts
@@ -0,0 +1,17 @@
+import { createContext, useContext } from "react";
+
+export type BreadcrumbItem = { label: string; to?: string };
+
+type BreadcrumbContextValue = {
+ trail: BreadcrumbItem[];
+ setTrail: (trail: BreadcrumbItem[]) => void;
+};
+
+export const BreadcrumbContext = createContext({
+ trail: [],
+ setTrail: () => {},
+});
+
+export function useBreadcrumbTrail(): BreadcrumbItem[] {
+ return useContext(BreadcrumbContext).trail;
+}
diff --git a/web/src/shell/breadcrumb-provider.tsx b/web/src/shell/breadcrumb-provider.tsx
new file mode 100644
index 0000000..9bbca29
--- /dev/null
+++ b/web/src/shell/breadcrumb-provider.tsx
@@ -0,0 +1,12 @@
+import { useState, type ReactNode } from "react";
+
+import { BreadcrumbContext, type BreadcrumbItem } from "./breadcrumb-context";
+
+export function BreadcrumbProvider({ children }: { children: ReactNode }) {
+ const [trail, setTrail] = useState([]);
+ return (
+
+ {children}
+
+ );
+}
diff --git a/web/src/shell/breadcrumb.test.tsx b/web/src/shell/breadcrumb.test.tsx
new file mode 100644
index 0000000..2934a63
--- /dev/null
+++ b/web/src/shell/breadcrumb.test.tsx
@@ -0,0 +1,26 @@
+import { expect, test } from "vitest";
+import { screen } from "@testing-library/react";
+import { renderApp } from "../test/render";
+import { BreadcrumbProvider } from "./breadcrumb-provider";
+import { Breadcrumb } from "./breadcrumb";
+import { useBreadcrumb } from "./use-breadcrumb";
+
+function Setter() {
+ useBreadcrumb([
+ { label: "Objects", to: "/objects" },
+ { label: "LM-0042" },
+ ]);
+ return null;
+}
+
+test("renders the trail with a link on non-leaf crumbs", async () => {
+ renderApp(
+
+
+
+ ,
+ );
+ const link = await screen.findByRole("link", { name: "Objects" });
+ expect(link).toHaveAttribute("href", "/objects");
+ expect(screen.getByText("LM-0042")).toBeInTheDocument();
+});
diff --git a/web/src/shell/breadcrumb.tsx b/web/src/shell/breadcrumb.tsx
new file mode 100644
index 0000000..68ed412
--- /dev/null
+++ b/web/src/shell/breadcrumb.tsx
@@ -0,0 +1,30 @@
+import { Fragment } from "react";
+import { Link } from "react-router-dom";
+
+import { useBreadcrumbTrail } from "./breadcrumb-context";
+
+export function Breadcrumb() {
+ const trail = useBreadcrumbTrail();
+ if (trail.length === 0) return ;
+ return (
+
+ );
+}
diff --git a/web/src/shell/use-breadcrumb.ts b/web/src/shell/use-breadcrumb.ts
new file mode 100644
index 0000000..042d3ed
--- /dev/null
+++ b/web/src/shell/use-breadcrumb.ts
@@ -0,0 +1,12 @@
+import { useContext, useEffect } from "react";
+
+import { BreadcrumbContext, type BreadcrumbItem } from "./breadcrumb-context";
+
+export function useBreadcrumb(trail: BreadcrumbItem[]): void {
+ const { setTrail } = useContext(BreadcrumbContext);
+ const key = trail.map((i) => `${i.label} ${i.to ?? ""}`).join("");
+ useEffect(() => {
+ setTrail(trail);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [key, setTrail]);
+}