huskies: merge 488_story_web_ui_shows_project_name_in_browser_tab_with_huskies_favicon
This commit is contained in:
+21
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 |
@@ -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");
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user