Feat: add page titles and favicon support

This commit is contained in:
MangoPig
2026-07-06 18:01:26 +01:00
parent dc081ccc5b
commit 91b4c254ec
34 changed files with 145 additions and 4 deletions
+1
View File
@@ -6,6 +6,7 @@ dist/
# dependencies
node_modules/
Frontend/.pnpm-store/
# logs
npm-debug.log*
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<square310x310logo src="/mstile-310x310.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+19
View File
@@ -0,0 +1,19 @@
{
"name": "Moku Work",
"short_name": "Moku Work",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicon/mstile-150x150.png"/>
<square310x310logo src="/favicon/mstile-310x310.png"/>
<TileColor>#ffffff</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+19
View File
@@ -0,0 +1,19 @@
{
"name": "Moku Work",
"short_name": "Moku Work",
"icons": [
{
"src": "/favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
@@ -1,11 +1,11 @@
// Path: Frontend/src/components/app-shell/AppShell/AppShell.tsx
import { useLocation } from "@solidjs/router";
import { createSignal, onCleanup, onMount, Show, type JSX } from "solid-js";
import { createEffect, createSignal, onCleanup, onMount, Show, type JSX } from "solid-js";
import { getDocumentTheme, setTheme, type Theme } from "../../../helper/themeRuntime";
import { BootstrapWizard } from "../../bootstrap/BootstrapWizard/BootstrapWizard";
import { AppShellDataProvider, useAppShellData } from "../data/app-shell.context";
import { getWorkspaceBreadcrumbSegments } from "../data/workspace-routes";
import { getWorkspaceBreadcrumbSegments, getWorkspaceDocumentTitle } from "../data/workspace-routes";
import { LeftRail } from "../../workspace-navigation/LeftRail/LeftRail";
import { MobileBottomNav } from "../MobileBottomNav/MobileBottomNav";
import { MobileWorkspaceBrowser } from "../../workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser";
@@ -94,6 +94,16 @@ const AppShellContent = (props: { children: JSX.Element }): JSX.Element => {
}),
].join(" / ");
createEffect((): void => {
if (typeof document === "undefined") {
return;
}
document.title = getWorkspaceDocumentTitle(location.pathname, {
activeProjectName: appShellData.activeProject().name,
});
});
return (
<div class={styles.shell} data-ui="app-shell" data-app-shell-status={appShellData.status()}>
<TopBar
@@ -1,6 +1,7 @@
// Path: Frontend/src/components/app-shell/data/workspace-routes.ts
import type { WorkspaceStaticKind } from "./shell.types";
import { buildPageTitle } from "../../../helper/pageTitle";
export type WorkspaceSurfaceKind = Exclude<WorkspaceStaticKind, "workspace">;
export type SettingsSectionKind = "account" | "workspace" | "server" | "theme" | "security" | "members";
@@ -11,6 +12,11 @@ export type WorkspaceBreadcrumbContext = {
activeTeamName?: string;
};
export type WorkspaceTitleContext = {
activeProjectName: string;
appName?: string;
};
const workspaceSurfaceRoutes: Record<WorkspaceSurfaceKind, string> = {
home: "/workspace/home",
settings: "/settings",
@@ -77,3 +83,38 @@ export const getWorkspaceBreadcrumbSegments = (
return [context.activeProjectName, getWorkspaceSurfaceBreadcrumbLabel(pathname)];
};
export const getWorkspaceDocumentTitle = (
pathname: string,
context: WorkspaceTitleContext,
): string => {
const appName = context.appName?.trim() || undefined;
const settingsSection = getSettingsSectionFromPathname(pathname);
if (pathname === workspaceSurfaceRoutes.settings || pathname === "/workspace/settings") {
return buildPageTitle(appName, "Settings");
}
if (settingsSection) {
switch (settingsSection) {
case "account":
return buildPageTitle(appName, "Account Settings");
case "workspace":
return buildPageTitle(appName, "Workspace Settings");
case "server":
return buildPageTitle(appName, "Server Settings");
case "theme":
return buildPageTitle(appName, "Theme Settings");
case "security":
return buildPageTitle(appName, "Security Settings");
case "members":
return buildPageTitle(appName, "Members & Roles");
}
}
if (pathname.startsWith(workspaceSurfaceRoutes.home)) {
return buildPageTitle(appName, context.activeProjectName);
}
return buildPageTitle(appName);
};
+9 -1
View File
@@ -34,7 +34,15 @@ const renderDocument = ({ assets, children, scripts }: DocumentRenderProps): JSX
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" type="image/x-icon" href="/favicon/favicon.ico" />
<link rel="icon" type="image/png" sizes="48x48" href="/favicon/favicon-48x48.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="theme-color" content="#ffffff" />
<script innerHTML={themeBootstrapScript} />
{assets}
</head>
+23
View File
@@ -0,0 +1,23 @@
// Path: Frontend/src/helper/pageTitle.ts
export const APP_PAGE_TITLE = "Moku Work";
const normalizeTitleSegment = (segment: string | null | undefined | false): string | null => {
const value = typeof segment === "string" ? segment.trim() : "";
return value.length > 0 ? value : null;
};
export const buildPageTitle = (...segments: Array<string | null | undefined | false>): string => {
const normalizedSegments = segments.map(normalizeTitleSegment).filter((segment): segment is string => segment !== null);
return [APP_PAGE_TITLE, ...normalizedSegments].join(" · ");
};
export const setPageTitle = (...segments: Array<string | null | undefined | false>): string => {
const title = buildPageTitle(...segments);
if (typeof document !== "undefined") {
document.title = title;
}
return title;
};