Feat: add page titles and favicon support
@@ -6,6 +6,7 @@ dist/
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
Frontend/.pnpm-store/
|
||||||
|
|
||||||
# logs
|
# logs
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
@@ -28,4 +29,4 @@ bin/
|
|||||||
|
|
||||||
.cgcignore
|
.cgcignore
|
||||||
|
|
||||||
POSIX/
|
POSIX/
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -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>
|
||||||
|
After Width: | Height: | Size: 459 B |
|
After Width: | Height: | Size: 874 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -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"
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 664 B |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -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>
|
||||||
|
After Width: | Height: | Size: 459 B |
|
After Width: | Height: | Size: 874 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -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
|
// Path: Frontend/src/components/app-shell/AppShell/AppShell.tsx
|
||||||
|
|
||||||
import { useLocation } from "@solidjs/router";
|
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 { getDocumentTheme, setTheme, type Theme } from "../../../helper/themeRuntime";
|
||||||
import { BootstrapWizard } from "../../bootstrap/BootstrapWizard/BootstrapWizard";
|
import { BootstrapWizard } from "../../bootstrap/BootstrapWizard/BootstrapWizard";
|
||||||
import { AppShellDataProvider, useAppShellData } from "../data/app-shell.context";
|
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 { LeftRail } from "../../workspace-navigation/LeftRail/LeftRail";
|
||||||
import { MobileBottomNav } from "../MobileBottomNav/MobileBottomNav";
|
import { MobileBottomNav } from "../MobileBottomNav/MobileBottomNav";
|
||||||
import { MobileWorkspaceBrowser } from "../../workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser";
|
import { MobileWorkspaceBrowser } from "../../workspace-navigation/MobileWorkspaceBrowser/MobileWorkspaceBrowser";
|
||||||
@@ -94,6 +94,16 @@ const AppShellContent = (props: { children: JSX.Element }): JSX.Element => {
|
|||||||
}),
|
}),
|
||||||
].join(" / ");
|
].join(" / ");
|
||||||
|
|
||||||
|
createEffect((): void => {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = getWorkspaceDocumentTitle(location.pathname, {
|
||||||
|
activeProjectName: appShellData.activeProject().name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.shell} data-ui="app-shell" data-app-shell-status={appShellData.status()}>
|
<div class={styles.shell} data-ui="app-shell" data-app-shell-status={appShellData.status()}>
|
||||||
<TopBar
|
<TopBar
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Path: Frontend/src/components/app-shell/data/workspace-routes.ts
|
// Path: Frontend/src/components/app-shell/data/workspace-routes.ts
|
||||||
|
|
||||||
import type { WorkspaceStaticKind } from "./shell.types";
|
import type { WorkspaceStaticKind } from "./shell.types";
|
||||||
|
import { buildPageTitle } from "../../../helper/pageTitle";
|
||||||
|
|
||||||
export type WorkspaceSurfaceKind = Exclude<WorkspaceStaticKind, "workspace">;
|
export type WorkspaceSurfaceKind = Exclude<WorkspaceStaticKind, "workspace">;
|
||||||
export type SettingsSectionKind = "account" | "workspace" | "server" | "theme" | "security" | "members";
|
export type SettingsSectionKind = "account" | "workspace" | "server" | "theme" | "security" | "members";
|
||||||
@@ -11,6 +12,11 @@ export type WorkspaceBreadcrumbContext = {
|
|||||||
activeTeamName?: string;
|
activeTeamName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkspaceTitleContext = {
|
||||||
|
activeProjectName: string;
|
||||||
|
appName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const workspaceSurfaceRoutes: Record<WorkspaceSurfaceKind, string> = {
|
const workspaceSurfaceRoutes: Record<WorkspaceSurfaceKind, string> = {
|
||||||
home: "/workspace/home",
|
home: "/workspace/home",
|
||||||
settings: "/settings",
|
settings: "/settings",
|
||||||
@@ -77,3 +83,38 @@ export const getWorkspaceBreadcrumbSegments = (
|
|||||||
|
|
||||||
return [context.activeProjectName, getWorkspaceSurfaceBreadcrumbLabel(pathname)];
|
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);
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,7 +34,15 @@ const renderDocument = ({ assets, children, scripts }: DocumentRenderProps): JSX
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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} />
|
<script innerHTML={themeBootstrapScript} />
|
||||||
{assets}
|
{assets}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||