diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1e4417..97e03a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to - 🐛(backend) allow any printable ASCII characters in user sub field #1673 - 🐛(frontend) keep the sending resolution picked while the camera is off #1667 - 🐛(frontend) restore automatic lower-hand on speaking +- 🐛(frontend) center Avatar initials with a font-aware cap-height ratio ## [1.30.0] - 2026-09-01 diff --git a/docker/dinum-frontend/dinum-styles.css b/docker/dinum-frontend/dinum-styles.css index d12ae9a3..80ea84e0 100644 --- a/docker/dinum-frontend/dinum-styles.css +++ b/docker/dinum-frontend/dinum-styles.css @@ -1,5 +1,6 @@ :root { --fonts-sans: 'Marianne', ui-sans-serif, system-ui, sans-serif; + --avatar-cap-height: 0.7; } .Header-beforeLogo { diff --git a/docs/theming.md b/docs/theming.md index 4d40658a..4b7858cb 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -34,6 +34,7 @@ Let's say you want to change the font of our application to a custom font. You c :root { --fonts-sans: 'Roboto', ui-sans-serif, system-ui, sans-serif; + --avatar-cap-height: 0.7; } ``` diff --git a/src/frontend/src/components/Avatar.tsx b/src/frontend/src/components/Avatar.tsx index 36bf7257..f3676026 100644 --- a/src/frontend/src/components/Avatar.tsx +++ b/src/frontend/src/components/Avatar.tsx @@ -1,5 +1,5 @@ import { css, cva, RecipeVariantProps } from '@/styled-system/css' -import React, { useLayoutEffect, useMemo } from 'react' +import React, { useMemo } from 'react' const avatar = cva({ base: { @@ -28,24 +28,17 @@ const avatar = cva({ }, }) -// Instantiating a segmenter is expensive; create it once and reuse it. const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl ? new Intl.Segmenter(undefined, { granularity: 'grapheme' }) : undefined -/** - * Returns the first user-perceived character. Some Unicode characters span - * multiple UTF-16 code units, so a naive index into the string can split them - * and yield a broken glyph. - */ const getFirstGrapheme = (value: string): string => { if (!value) return '' if (graphemeSegmenter) { const [first] = graphemeSegmenter.segment(value) return first?.segment ?? '' } - // Fallback: keeps single code points intact (including surrogate pairs). return Array.from(value)[0] ?? '' } @@ -66,36 +59,6 @@ export type AvatarProps = React.HTMLAttributes & { export const Avatar = React.memo( ({ name, bgColor, context, notification, style, ...props }: AvatarProps) => { const initials = useMemo(() => getInitials(name), [name]) - const textRef = React.useRef(null) - const [offsetY, setOffsetY] = React.useState(0) - - // Optically center the initials: measure the ink bounding box of the - // rendered glyphs and shift them so the box's center sits at the middle - // of the viewBox. Works for any font, weight or glyph shape, unlike a - // hand-tuned dy offset. getBBox() is in local (pre-transform) - // coordinates, so applying the translation never changes the measure. - useLayoutEffect(() => { - const text = textRef.current - if (!text) return - - const center = () => { - const box = text.getBBox() - // A hidden element measures as an empty box; keep the default then. - if (box.height === 0) return - setOffsetY(50 - (box.y + box.height / 2)) - } - - center() - // Glyph metrics can change once webfonts finish loading. - let cancelled = false - document.fonts?.ready.then(() => { - if (!cancelled) center() - }) - return () => { - cancelled = true - } - }, [initials]) - return (
{initials} diff --git a/src/frontend/src/styles/index.css b/src/frontend/src/styles/index.css index 88e67d56..758ed6f3 100644 --- a/src/frontend/src/styles/index.css +++ b/src/frontend/src/styles/index.css @@ -6,6 +6,10 @@ body, height: 100%; } +:root { + --avatar-cap-height: 0.7; +} + html.font-lexend { --fonts-sans: 'Lexend Variable', ui-sans-serif, system-ui, sans-serif; }