feat(header): shrink long titles to fit instead of wrapping

The header keeps its size; a title that would overflow scales down to a
floor of 55% and ellipsizes only past that. Re-measures on title change,
resize, and web-font load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189ASmkMee4F5aJ3jnKeKQv
This commit is contained in:
2026-08-23 20:06:05 -07:00
co-authored by Claude Fable 5
parent a12d078323
commit ed5eaa057c
+56 -2
View File
@@ -1,15 +1,57 @@
import { useLayoutEffect, useRef } from "react";
import { Title, AppShell, Flex } from "@mantine/core"; import { Title, AppShell, Flex } from "@mantine/core";
import { useLingui } from "@lingui/react"; import { useLingui } from "@lingui/react";
import { HeaderConfig } from "../types/header-config"; import { HeaderConfig } from "../types/header-config";
import BackButton from "./back-button"; import BackButton from "./back-button";
const MIN_TITLE_SCALE = 0.55;
const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig) => { const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig) => {
const { i18n } = useLingui(); const { i18n } = useLingui();
const titleRef = useRef<HTMLHeadingElement>(null);
const resolvedTitle = const resolvedTitle =
typeof title === "string" || title === undefined typeof title === "string" || title === undefined
? title ? title
: i18n._({ ...title, values: titleValues }); : i18n._({ ...title, values: titleValues });
useLayoutEffect(() => {
const el = titleRef.current;
if (!el) return;
let cancelled = false;
const textWidth = () => {
const range = document.createRange();
range.selectNodeContents(el);
return range.getBoundingClientRect().width;
};
const fit = () => {
el.style.fontSize = "";
const available = el.clientWidth - 1;
if (available <= 0) return;
let needed = textWidth();
if (needed <= available) return;
const natural = parseFloat(getComputedStyle(el).fontSize);
let scale = Math.max(available / needed, MIN_TITLE_SCALE);
el.style.fontSize = `${natural * scale}px`;
for (let i = 0; i < 3 && scale > MIN_TITLE_SCALE && textWidth() > available; i++) {
scale = Math.max(scale * 0.98, MIN_TITLE_SCALE);
el.style.fontSize = `${natural * scale}px`;
}
};
fit();
document.fonts?.ready.then(() => {
if (!cancelled) fit();
});
const observer = new ResizeObserver(fit);
observer.observe(el);
return () => {
cancelled = true;
observer.disconnect();
};
}, [resolvedTitle]);
return ( return (
<AppShell.Header <AppShell.Header
id='app-header' id='app-header'
@@ -20,8 +62,20 @@ const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig)
}} }}
> >
{ withBackButton && <BackButton /> } { withBackButton && <BackButton /> }
<Flex justify='center' px='md' mt={8}> <Flex justify='center' px={48} mt={8} w='100%'>
<Title order={1} lts='0.08em' style={{ userSelect: 'none' }}> <Title
ref={titleRef}
order={1}
lts='0.08em'
style={{
userSelect: 'none',
width: '100%',
textAlign: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{resolvedTitle?.toLocaleUpperCase()} {resolvedTitle?.toLocaleUpperCase()}
</Title> </Title>
</Flex> </Flex>