From c342b9102945bc8cb4f255c9d69c652be0a9b797 Mon Sep 17 00:00:00 2001 From: yohlo Date: Sun, 23 Aug 2026 20:06:05 -0700 Subject: [PATCH] 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. --- src/features/core/components/header.tsx | 58 ++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/features/core/components/header.tsx b/src/features/core/components/header.tsx index e393091..6aca616 100644 --- a/src/features/core/components/header.tsx +++ b/src/features/core/components/header.tsx @@ -1,15 +1,57 @@ +import { useLayoutEffect, useRef } from "react"; import { Title, AppShell, Flex } from "@mantine/core"; import { useLingui } from "@lingui/react"; import { HeaderConfig } from "../types/header-config"; import BackButton from "./back-button"; +const MIN_TITLE_SCALE = 0.55; + const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig) => { const { i18n } = useLingui(); + const titleRef = useRef(null); const resolvedTitle = typeof title === "string" || title === undefined ? title : 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 ( { withBackButton && } - - + <Flex justify='center' px={48} mt={8} w='100%'> + <Title + ref={titleRef} + order={1} + lts='0.08em' + style={{ + userSelect: 'none', + width: '100%', + textAlign: 'center', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }} + > {resolvedTitle?.toLocaleUpperCase()}