huskies: merge 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon

This commit is contained in:
dave
2026-04-07 13:33:06 +00:00
parent c64577eff0
commit 4476c57444
5 changed files with 121 additions and 1 deletions
@@ -0,0 +1,21 @@
---
name: "Web UI shows project name in browser tab with huskies favicon"
---
# Story 488: Web UI shows project name in browser tab with huskies favicon
## User Story
As a user running huskies on multiple projects, I want the browser tab to show the project name (e.g. "Reclaimer") instead of the hardcoded "Huskies", and I want a huskies favicon derived from the website logo, so I can distinguish tabs and have proper branding.
## Acceptance Criteria
- [ ] Browser tab title shows the project folder name when a project is open (e.g. `/home/user/reclaimer``reclaimer | Huskies`)
- [ ] Browser tab title shows `Huskies` when no project is open
- [ ] A huskies-themed SVG favicon is served and shown in the browser tab
- [ ] The Vite default favicon is replaced by the huskies favicon
## Out of Scope
- Fetching a user-configured display name from the backend (folder name is sufficient)
- Changing the app name shown in the UI heading
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/huskies.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Huskies</title>
</head>
+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Left ear -->
<polygon points="5,14 8,4 13,13" fill="#4b5563"/>
<polygon points="6.5,12.5 8.5,6 12,12" fill="#9ca3af"/>
<!-- Right ear -->
<polygon points="27,14 24,4 19,13" fill="#4b5563"/>
<polygon points="25.5,12.5 23.5,6 20,12" fill="#9ca3af"/>
<!-- Head -->
<circle cx="16" cy="18" r="12" fill="#6b7280"/>
<!-- White face mask -->
<ellipse cx="16" cy="21" rx="8" ry="7" fill="#f9fafb"/>
<!-- Left eye white -->
<circle cx="12" cy="16" r="3" fill="white"/>
<!-- Left eye iris - blue (husky trait) -->
<circle cx="12.3" cy="16" r="2" fill="#3b82f6"/>
<!-- Left eye pupil -->
<circle cx="12.3" cy="16" r="1" fill="#111827"/>
<!-- Left eye highlight -->
<circle cx="11.7" cy="15.3" r="0.5" fill="white"/>
<!-- Right eye white -->
<circle cx="20" cy="16" r="3" fill="white"/>
<!-- Right eye iris - blue -->
<circle cx="20.3" cy="16" r="2" fill="#3b82f6"/>
<!-- Right eye pupil -->
<circle cx="20.3" cy="16" r="1" fill="#111827"/>
<!-- Right eye highlight -->
<circle cx="19.7" cy="15.3" r="0.5" fill="white"/>
<!-- Nose -->
<ellipse cx="16" cy="22" rx="2.5" ry="1.8" fill="#1f2937"/>
<!-- Nose highlight -->
<ellipse cx="15.3" cy="21.3" rx="0.7" ry="0.5" fill="#6b7280"/>
<!-- Mouth line -->
<path d="M16,23.5 Q14,25 13,24.5" stroke="#9ca3af" stroke-width="0.6" fill="none" stroke-linecap="round"/>
<path d="M16,23.5 Q18,25 19,24.5" stroke="#9ca3af" stroke-width="0.6" fill="none" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+53
View File
@@ -345,6 +345,59 @@ describe("App", () => {
});
});
it("sets document title to Huskies when no project is open", async () => {
mockedApi.getCurrentProject.mockResolvedValue(null);
await renderApp();
await waitFor(() => {
expect(document.title).toBe("Huskies");
});
});
it("sets document title to project name when a project is open", async () => {
mockedApi.getCurrentProject.mockResolvedValue("/home/user/reclaimer");
await renderApp();
await waitFor(() => {
expect(document.title).toBe("reclaimer | Huskies");
});
});
it("resets document title to Huskies after closing project", async () => {
mockedApi.openProject.mockResolvedValue("/home/user/myproject");
mockedApi.closeProject.mockResolvedValue(true);
await renderApp();
await waitFor(() => {
expect(
screen.getByPlaceholderText(/\/path\/to\/project/i),
).toBeInTheDocument();
});
const input = screen.getByPlaceholderText(
/\/path\/to\/project/i,
) as HTMLInputElement;
await userEvent.clear(input);
await userEvent.type(input, "/home/user/myproject");
const openButton = screen.getByRole("button", { name: /open project/i });
await userEvent.click(openButton);
await waitFor(() => {
expect(document.title).toBe("myproject | Huskies");
});
const closeButton = await waitFor(() => screen.getByText("✕"));
await userEvent.click(closeButton);
await waitFor(() => {
expect(document.title).toBe("Huskies");
});
});
it("handles Enter key to trigger project open", async () => {
mockedApi.openProject.mockResolvedValue("/home/user/myproject");
+11
View File
@@ -51,6 +51,17 @@ function App() {
});
}, []);
React.useEffect(() => {
if (projectPath) {
const projectName =
projectPath.replace(/\\/g, "/").split("/").filter(Boolean).pop() ??
projectPath;
document.title = `${projectName} | Huskies`;
} else {
document.title = "Huskies";
}
}, [projectPath]);
React.useEffect(() => {
api
.getKnownProjects()