diff --git a/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css b/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css
index f4ae1fdc4..1f2fb81c8 100644
--- a/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css
+++ b/fluxer_app/src/features/app/components/layout/GuildsLayout.module.css
@@ -206,6 +206,7 @@
color 70ms ease-out;
container-type: size;
--initials-font-size: clamp(0.85rem, 45cqi, 1.35rem);
+ --initials-fit-inline-size: 92cqi;
--initials-letter-spacing: 0px;
--initials-scale: 1;
}
@@ -259,7 +260,10 @@
line-height: 1;
color: inherit;
font-weight: 600;
- font-size: var(--initials-font-size);
+ font-size: min(
+ var(--initials-font-size),
+ calc(var(--initials-fit-inline-size) / (var(--initials-width-em, 0.01) * var(--initials-scale)))
+ );
letter-spacing: var(--initials-letter-spacing);
transform: scale(var(--initials-scale));
transform-origin: center;
@@ -271,12 +275,6 @@
--initials-scale: 0.92;
}
-.guildIcon[data-initials-length='long'] .guildIconInitials {
- --initials-font-size: clamp(0.85rem, 32cqi, 0.87rem);
- --initials-letter-spacing: 0px;
- --initials-scale: 0.78;
-}
-
.guildIndicator {
position: absolute;
left: var(--guild-list-indicator-inset);
diff --git a/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemPresentation.tsx b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemPresentation.tsx
index 136a2f52e..19b4eb7ed 100644
--- a/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemPresentation.tsx
+++ b/fluxer_app/src/features/app/components/layout/sidebar_nav/GuildListItemPresentation.tsx
@@ -14,6 +14,7 @@ import Guilds from '@app/features/guild/state/Guilds';
import {
type GuildInitialsLength,
getGuildIconDisplayInitials,
+ getGuildInitialsFitStyle,
getInitialsFromName,
getInitialsLength,
truncateInitials,
@@ -57,11 +58,11 @@ function resolveGuildListItemMotion(useReducedMotion: boolean): GuildListItemMot
return STANDARD_GUILD_LIST_ITEM_MOTION;
}
-function resolveInitialsLength(rawInitials: string): GuildInitialsLength | null {
- if (rawInitials.length === 0) {
+function resolveInitialsLength(displayInitials: string): GuildInitialsLength | null {
+ if (displayInitials.length === 0) {
return null;
}
- return getInitialsLength(rawInitials);
+ return getInitialsLength(displayInitials);
}
function resolveDraggingCursor(dragAndDrop: GuildListItemDragAndDrop | null): 'grabbing' | null {
@@ -303,8 +304,8 @@ export const GuildListItemPresentation = forwardRef<
forwardedRef,
) {
const rootRef = useMergeRefs([surfaceRef, forwardedRef]);
- const rawInitials = getInitialsFromName(guild.name);
- const initialsLength = resolveInitialsLength(rawInitials);
+ const displayInitials = getGuildIconDisplayInitials(getInitialsFromName(guild.name));
+ const initialsLength = resolveInitialsLength(displayInitials);
const dropIndicator = resolveDropIndicator(dragAndDrop);
const motion = resolveGuildListItemMotion(Accessibility.useReducedMotion);
const draggingCursor = resolveDraggingCursor(dragAndDrop);
@@ -378,9 +379,10 @@ export const GuildListItemPresentation = forwardRef<
{!hasPaintedIcon && (
- {getGuildIconDisplayInitials(rawInitials)}
+ {displayInitials}
)}
diff --git a/fluxer_app/src/features/guild/components/modals/AddGuildModal.module.css b/fluxer_app/src/features/guild/components/modals/AddGuildModal.module.css
index d73e3d7ca..fed53f749 100644
--- a/fluxer_app/src/features/guild/components/modals/AddGuildModal.module.css
+++ b/fluxer_app/src/features/guild/components/modals/AddGuildModal.module.css
@@ -133,6 +133,7 @@
background: var(--background-tertiary);
container-type: size;
--initials-font-size: clamp(0.85rem, 45cqi, 1.35rem);
+ --initials-fit-inline-size: 92cqi;
--initials-letter-spacing: 0px;
--initials-scale: 1;
}
@@ -146,7 +147,10 @@
white-space: nowrap;
display: inline-block;
text-align: center;
- font-size: var(--initials-font-size);
+ font-size: min(
+ var(--initials-font-size),
+ calc(var(--initials-fit-inline-size) / (var(--initials-width-em, 0.01) * var(--initials-scale)))
+ );
letter-spacing: var(--initials-letter-spacing);
transform: scale(var(--initials-scale));
transform-origin: center;
@@ -158,12 +162,6 @@
--initials-scale: 0.92;
}
-.iconPlaceholder[data-initials-length='long'] .iconInitials {
- --initials-font-size: clamp(0.85rem, 32cqi, 0.87rem);
- --initials-letter-spacing: 0px;
- --initials-scale: 0.78;
-}
-
.iconActions {
display: flex;
flex: 1;
diff --git a/fluxer_app/src/features/guild/components/modals/add_guild_modal/GuildCreateForm.tsx b/fluxer_app/src/features/guild/components/modals/add_guild_modal/GuildCreateForm.tsx
index 9b18e3ac8..efb1f3a58 100644
--- a/fluxer_app/src/features/guild/components/modals/add_guild_modal/GuildCreateForm.tsx
+++ b/fluxer_app/src/features/guild/components/modals/add_guild_modal/GuildCreateForm.tsx
@@ -30,7 +30,11 @@ import {
ICON_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR,
ModalFooterContext,
} from '@app/features/guild/components/modals/add_guild_modal/shared';
-import {getGuildIconDisplayInitials, getInitialsLength} from '@app/features/guild/utils/GuildInitialsUtils';
+import {
+ getGuildIconDisplayInitials,
+ getGuildInitialsFitStyle,
+ getInitialsLength,
+} from '@app/features/guild/utils/GuildInitialsUtils';
import {
CREATE_COMMUNITY_DESCRIPTOR,
FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR,
@@ -75,13 +79,12 @@ export const GuildCreateForm = observer(() => {
const currentUser = Users.currentUser;
const shouldRequireClaimedAccount = currentUser != null && !currentUser.isClaimed();
const nameValue = form.watch('name');
- const rawInitials = useMemo(() => {
+ const initials = useMemo(() => {
const raw = (nameValue || '').trim();
if (!raw) return '';
- return StringUtils.getInitialsFromName(raw);
+ return getGuildIconDisplayInitials(StringUtils.getInitialsFromName(raw));
}, [nameValue]);
- const initials = useMemo(() => getGuildIconDisplayInitials(rawInitials), [rawInitials]);
- const initialsLength = useMemo(() => (rawInitials ? getInitialsLength(rawInitials) : null), [rawInitials]);
+ const initialsLength = initials ? getInitialsLength(initials) : null;
const showIconUploadErrorModal = useCallback(
(message: string) => {
showGuildErrorModal({
@@ -289,6 +292,7 @@ export const GuildCreateForm = observer(() => {
{initials ? (
{initials}
diff --git a/fluxer_app/src/features/guild/components/modals/add_guild_modal/TemplateImportForm.tsx b/fluxer_app/src/features/guild/components/modals/add_guild_modal/TemplateImportForm.tsx
index 6ff49ace1..32a69211a 100644
--- a/fluxer_app/src/features/guild/components/modals/add_guild_modal/TemplateImportForm.tsx
+++ b/fluxer_app/src/features/guild/components/modals/add_guild_modal/TemplateImportForm.tsx
@@ -44,7 +44,11 @@ import {
type TemplateJsonFormInputs,
THIS_DOESN_T_LOOK_LIKE_A_VALID_TEMPLATE_DESCRIPTOR,
} from '@app/features/guild/components/modals/add_guild_modal/shared';
-import {getGuildIconDisplayInitials, getInitialsLength} from '@app/features/guild/utils/GuildInitialsUtils';
+import {
+ getGuildIconDisplayInitials,
+ getGuildInitialsFitStyle,
+ getInitialsLength,
+} from '@app/features/guild/utils/GuildInitialsUtils';
import {
CREATE_COMMUNITY_DESCRIPTOR,
FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR,
@@ -245,13 +249,12 @@ export const TemplateImportForm = observer(() => {
const nameValue = createForm.watch('name');
const urlValue = urlForm.watch('url');
const jsonValue = jsonForm.watch('json');
- const rawInitials = useMemo(() => {
+ const initials = useMemo(() => {
const raw = (nameValue || '').trim();
if (!raw) return '';
- return StringUtils.getInitialsFromName(raw);
+ return getGuildIconDisplayInitials(StringUtils.getInitialsFromName(raw));
}, [nameValue]);
- const initials = useMemo(() => getGuildIconDisplayInitials(rawInitials), [rawInitials]);
- const initialsLength = useMemo(() => (rawInitials ? getInitialsLength(rawInitials) : null), [rawInitials]);
+ const initialsLength = initials ? getInitialsLength(initials) : null;
const showIconUploadErrorModal = useCallback(
(message: string) => {
showGuildErrorModal({
@@ -612,6 +615,7 @@ export const TemplateImportForm = observer(() => {
{initials ? (
{initials}
diff --git a/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css b/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css
index 24a462dec..5ecd9d6cb 100644
--- a/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css
+++ b/fluxer_app/src/features/guild/components/popouts/GuildIcon.module.css
@@ -19,6 +19,7 @@
background-size: cover;
container-type: inline-size;
--initials-font-size: clamp(0.85rem, 45cqi, 1.35rem);
+ --initials-fit-inline-size: 92cqi;
--initials-letter-spacing: 0px;
--initials-scale: 1;
--initials-scale-cap: 1;
@@ -35,7 +36,10 @@
white-space: nowrap;
display: inline-block;
text-align: center;
- font-size: var(--initials-font-size);
+ font-size: min(
+ var(--initials-font-size),
+ calc(var(--initials-fit-inline-size) / (var(--initials-width-em, 0.01) * var(--initials-scale)))
+ );
letter-spacing: var(--initials-letter-spacing);
transform: scale(calc(var(--initials-scale) * var(--initials-scale-cap)));
transform-origin: center;
@@ -47,12 +51,6 @@
--initials-letter-spacing: 0px;
}
-.container[data-initials-length='long'] .initials {
- --initials-font-size: clamp(0.85rem, 32cqi, 0.87rem);
- --initials-scale: 0.78;
- --initials-letter-spacing: 0px;
-}
-
@container (max-width: 32px) {
.initials {
--initials-scale-cap: 0.88;
diff --git a/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx b/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx
index f964fc966..8e7539808 100644
--- a/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx
+++ b/fluxer_app/src/features/guild/components/popouts/GuildIcon.tsx
@@ -3,7 +3,11 @@
import {useHover} from '@app/features/app/hooks/useHover';
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
import styles from '@app/features/guild/components/popouts/GuildIcon.module.css';
-import {getGuildIconDisplayInitials, getInitialsLength} from '@app/features/guild/utils/GuildInitialsUtils';
+import {
+ getGuildIconDisplayInitials,
+ getGuildInitialsFitStyle,
+ getInitialsLength,
+} from '@app/features/guild/utils/GuildInitialsUtils';
import {useRecoveringBackgroundImageURL} from '@app/features/messaging/hooks/useImageRecovery';
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
@@ -39,9 +43,8 @@ export const GuildIcon = observer(function GuildIcon({
sizePx,
containerProps,
}: GuildIconProps) {
- const rawInitials = useMemo(() => StringUtils.getInitialsFromName(name), [name]);
- const initials = useMemo(() => getGuildIconDisplayInitials(rawInitials), [rawInitials]);
- const initialsLength = useMemo(() => getInitialsLength(rawInitials), [rawInitials]);
+ const initials = useMemo(() => getGuildIconDisplayInitials(StringUtils.getInitialsFromName(name)), [name]);
+ const initialsLength = getInitialsLength(initials);
const [hoverRef, isHovering] = useHover();
const iconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}) : null;
const hoverIconUrl = icon ? AvatarUtils.getGuildIconURL({id, icon}, true) : null;
@@ -73,7 +76,11 @@ export const GuildIcon = observer(function GuildIcon({
style={styleVars}
>
{paintedUrl == null && (
-
+
{initials}
)}
diff --git a/fluxer_app/src/features/guild/utils/GuildInitialsUtils.ts b/fluxer_app/src/features/guild/utils/GuildInitialsUtils.ts
index 09128aed6..6e25e20c3 100644
--- a/fluxer_app/src/features/guild/utils/GuildInitialsUtils.ts
+++ b/fluxer_app/src/features/guild/utils/GuildInitialsUtils.ts
@@ -1,15 +1,25 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
+import type React from 'react';
+
const GUILD_ICON_INITIALS_MAX_LENGTH = 4;
+const MEASURE_FONT_SIZES_PX = [10, 12, 14, 16, 20, 24];
+const MEASURE_FONT_WEIGHT = 600;
export const GuildInitialsLength = Object.freeze({
SHORT: 'short',
MEDIUM: 'medium',
- LONG: 'long',
} as const);
export type GuildInitialsLength = (typeof GuildInitialsLength)[keyof typeof GuildInitialsLength];
+export type GuildInitialsFitStyle = React.CSSProperties & {
+ '--initials-width-em'?: string;
+};
+
+const measuredWidthsEm = new Map();
+let measureContext: OffscreenCanvasRenderingContext2D | null | undefined;
+
export function getInitialsFromName(name: string): string {
const words = name.split(/\s+/u).filter(Boolean);
return words
@@ -27,9 +37,40 @@ export function getGuildIconDisplayInitials(initials: string): string {
return truncateInitials(initials, GUILD_ICON_INITIALS_MAX_LENGTH);
}
-export function getInitialsLength(initials: string): GuildInitialsLength {
- const length = Array.from(initials).length;
- if (length <= 2) return GuildInitialsLength.SHORT;
- if (length <= 4) return GuildInitialsLength.MEDIUM;
- return GuildInitialsLength.LONG;
+export function getInitialsLength(displayInitials: string): GuildInitialsLength {
+ return Array.from(displayInitials).length <= 2 ? GuildInitialsLength.SHORT : GuildInitialsLength.MEDIUM;
+}
+
+function getMeasureContext(): OffscreenCanvasRenderingContext2D | null {
+ if (measureContext !== undefined) return measureContext;
+ if (typeof document === 'undefined' || typeof OffscreenCanvas === 'undefined') {
+ measureContext = null;
+ return measureContext;
+ }
+ measureContext = new OffscreenCanvas(1, 1).getContext('2d');
+ document.fonts?.addEventListener('loadingdone', () => measuredWidthsEm.clear());
+ return measureContext;
+}
+
+function measureInitialsWidthEm(initials: string): number | null {
+ const context = getMeasureContext();
+ if (context == null || document.body == null) return null;
+ const fontFamily = getComputedStyle(document.body).fontFamily;
+ const key = `${fontFamily}\u0000${initials}`;
+ const cached = measuredWidthsEm.get(key);
+ if (cached !== undefined) return cached;
+ let widthEm = 0;
+ for (const fontSize of MEASURE_FONT_SIZES_PX) {
+ context.font = `${MEASURE_FONT_WEIGHT} ${fontSize}px ${fontFamily}`;
+ widthEm = Math.max(widthEm, context.measureText(initials).width / fontSize);
+ }
+ measuredWidthsEm.set(key, widthEm);
+ return widthEm;
+}
+
+export function getGuildInitialsFitStyle(displayInitials: string): GuildInitialsFitStyle | undefined {
+ if (!displayInitials) return undefined;
+ const widthEm = measureInitialsWidthEm(displayInitials);
+ if (widthEm == null || widthEm <= 0) return undefined;
+ return {'--initials-width-em': widthEm.toFixed(3)};
}