diff --git a/src/app/pets/page.tsx b/src/app/pets/page.tsx
new file mode 100644
index 0000000..087cdc5
--- /dev/null
+++ b/src/app/pets/page.tsx
@@ -0,0 +1,34 @@
+import Link from "next/link";
+import { listPets } from "@/modules/pets/server/queries";
+
+export default async function PetsPage() {
+ const pets = await listPets();
+ return (
+
+
+
Pets
+
+ Add pet
+
+
+ {pets.length === 0 ? (
+
No pets yet.
+ ) : (
+
+ {pets.map((pet) => (
+
+
{pet.name}
+
+ {[pet.species, pet.breed].filter(Boolean).join(" ยท ")}
+
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/nav-icon.tsx b/src/components/nav-icon.tsx
index b332ed9..780454e 100644
--- a/src/components/nav-icon.tsx
+++ b/src/components/nav-icon.tsx
@@ -3,6 +3,7 @@ import {
BookHeart,
Calendar,
Sprout,
+ PawPrint,
CalendarDays,
CheckSquare,
ChevronDown,
@@ -73,6 +74,7 @@ const ICONS: Record> = {
filter: Filter,
sun: Sun,
sprout: Sprout,
+ "paw-print": PawPrint,
mail: Mail,
menu: Menu,
"message-circle": MessageCircle,
diff --git a/src/modules/index.ts b/src/modules/index.ts
index 50b7486..93d25ab 100644
--- a/src/modules/index.ts
+++ b/src/modules/index.ts
@@ -4,6 +4,7 @@ import calendarManifest from "./calendar/manifest";
import listsManifest from "./lists/manifest";
import notesManifest from "./notes/manifest";
import gardenManifest from "./garden/manifest";
+import petsManifest from "./pets/manifest";
import bangsManifest from "./bangs/manifest";
import journalManifest from "./journal/manifest";
import agentManifest from "./agent/manifest";
@@ -13,6 +14,7 @@ registerModule(calendarManifest);
registerModule(listsManifest);
registerModule(notesManifest);
registerModule(gardenManifest);
+registerModule(petsManifest);
registerModule(bangsManifest);
registerModule(journalManifest);
registerModule(agentManifest);
diff --git a/src/modules/pets/manifest.tsx b/src/modules/pets/manifest.tsx
new file mode 100644
index 0000000..2ca7b20
--- /dev/null
+++ b/src/modules/pets/manifest.tsx
@@ -0,0 +1,49 @@
+import { z } from "zod";
+import type { ModuleManifest } from "../_core/module";
+import { searchPets } from "./server/queries";
+
+const petsManifest: ModuleManifest = {
+ id: "pets",
+ name: "Pets",
+ nav: { href: "/pets", label: "Pets", icon: "paw-print" },
+ entities: [
+ {
+ type: "pets.pet",
+ label: { singular: "Pet", plural: "Pets" },
+ search: { search: searchPets },
+ reminder: { canRemind: true },
+ resolveUrl: (id) => `/pets/${id}`,
+ renderActivity: (entry) => {
+ const name = entry.payload?.name as string | undefined;
+ if (entry.action === "create") return `Added pet${name ? ` "${name}"` : ""}`;
+ if (entry.action === "delete") return `Removed pet${name ? ` "${name}"` : ""}`;
+ return `Updated pet${name ? ` "${name}"` : ""}`;
+ },
+ },
+ ],
+ dashboardWidgets: [
+ {
+ id: "pets.overview",
+ title: "Pets overview",
+ description: "Pet count and medical items due within 30 days.",
+ category: "Pets",
+ defaultSize: { w: 3, h: 2 },
+ minSize: { w: 2, h: 2 },
+ defaultPriority: 60,
+ configSchema: z.object({}),
+ defaultConfig: {},
+ render: () => null,
+ },
+ ],
+ quickAdds: [
+ {
+ id: "pets.add",
+ label: "Add pet",
+ icon: "paw-print",
+ url: "/pets/new",
+ createKey: "pets.pet",
+ },
+ ],
+};
+
+export default petsManifest;
diff --git a/tests/unit/pets-manifest.test.ts b/tests/unit/pets-manifest.test.ts
new file mode 100644
index 0000000..e690c78
--- /dev/null
+++ b/tests/unit/pets-manifest.test.ts
@@ -0,0 +1,8 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import petsManifest from "../../src/modules/pets/manifest";
+
+test("pets manifest contributes visible navigation and a quick add action", () => {
+ assert.deepEqual(petsManifest.nav, { href: "/pets", label: "Pets", icon: "paw-print" });
+ assert.equal(petsManifest.quickAdds?.[0]?.createKey, "pets.pet");
+});