mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 01:46:46 +08:00
Every dashboard tab rendered the root metadata title, so several issues open side by side were indistinguishable without clicking into each one (GH #7010). Web now names the tab from the URL, e.g. `MUL-123: Fix login | Multica`. The resolution is not new: `WorkspaceDocumentTitle` mounts once in the dashboard layout and reuses the pure subject parser plus the cache-backed presentation hook that already name desktop's tabs, so a browser tab and a desktop tab for one URL read identically, in the user's locale, and a renamed project or a changed issue title re-titles the tab live. Every query behind it is cache-only, so it costs no extra request. The site title and the `%s | Multica` template move next to it and are imported by the root layout's metadata, so the statically rendered title and the client-set one cannot drift into two separate separators. Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]>
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
/**
|
|
* The browser tab title contract for the web app.
|
|
*
|
|
* Two writers produce titles and they must agree: Next.js metadata renders the
|
|
* `<title>` for statically known routes (landing, auth), and
|
|
* `WorkspaceDocumentTitle` sets `document.title` on workspace routes, whose
|
|
* names only exist in the query cache. Both go through the constants here so
|
|
* `Issues | Multica` and `Changelog | Multica` can never drift into two
|
|
* different separators.
|
|
*
|
|
* Pure and React-free on purpose: the root layout is a server component and
|
|
* imports `SITE_TITLE` / `TITLE_TEMPLATE` for its metadata export.
|
|
*/
|
|
|
|
/** Root fallback — the title of a page that has nothing more specific to say. */
|
|
export const SITE_TITLE = "Multica — Project Management for Human + Agent Teams";
|
|
|
|
/** Appended to every page-specific title. */
|
|
export const TITLE_SUFFIX = " | Multica";
|
|
|
|
/** Next.js `metadata.title.template`; see apps/web/app/layout.tsx. */
|
|
export const TITLE_TEMPLATE = `%s${TITLE_SUFFIX}`;
|
|
|
|
/**
|
|
* Longest page name we put in front of the suffix, counted in code points.
|
|
*
|
|
* Issue titles are unbounded server-side, and the whole string ends up in the
|
|
* window title, history entries and bookmarks, not just the tab — a 600-char
|
|
* title makes all three unreadable. Browsers truncate the tab itself far
|
|
* earlier than this, so the cap only exists to keep those secondary surfaces
|
|
* sane; the discriminating part of a title (the issue key) leads, so it always
|
|
* survives both truncations.
|
|
*/
|
|
export const MAX_PAGE_TITLE_LENGTH = 120;
|
|
|
|
/**
|
|
* Clip to {@link MAX_PAGE_TITLE_LENGTH} code points.
|
|
*
|
|
* Iterating code points rather than slicing UTF-16 units keeps an emoji or any
|
|
* other astral character from being cut in half into a replacement glyph — a
|
|
* real case here, since issue titles routinely open with one.
|
|
*/
|
|
function clipTitle(title: string): string {
|
|
const codePoints = Array.from(title);
|
|
if (codePoints.length <= MAX_PAGE_TITLE_LENGTH) return title;
|
|
return `${codePoints.slice(0, MAX_PAGE_TITLE_LENGTH).join("").trimEnd()}…`;
|
|
}
|
|
|
|
/**
|
|
* Build the full document title for a page name, e.g.
|
|
* `MUL-123: Fix login` → `MUL-123: Fix login | Multica`.
|
|
*
|
|
* An empty or whitespace-only name falls back to {@link SITE_TITLE} rather than
|
|
* rendering a bare ` | Multica`.
|
|
*/
|
|
export function formatDocumentTitle(pageTitle: string | null | undefined): string {
|
|
const trimmed = pageTitle?.trim();
|
|
if (!trimmed) return SITE_TITLE;
|
|
return `${clipTitle(trimmed)}${TITLE_SUFFIX}`;
|
|
}
|