Compare commits

...

1 Commits

Author SHA1 Message Date
Teffen Ellis
0135914c93 web: Improve app title readability. 2025-10-31 17:35:25 +01:00
6 changed files with 89 additions and 5 deletions

View File

@@ -0,0 +1,47 @@
/**
* @file Text measurement utilities.
*/
/**
* Measures text dimensions using an offscreen canvas.
*/
export class TextMeasurer implements Disposable {
#ctx: OffscreenCanvasRenderingContext2D | null;
public get canvas(): OffscreenCanvas | null {
return this.#ctx?.canvas ?? null;
}
public get font(): string | null {
return this.#ctx?.font ?? null;
}
public set font(value: string) {
if (!this.#ctx) return;
this.#ctx!.font = value;
}
constructor(canvas: OffscreenCanvas = new OffscreenCanvas(300, 150)) {
this.#ctx = canvas.getContext("2d");
if (!this.#ctx) {
console.error("failed to get canvas context");
}
const styles = window.getComputedStyle(document.body);
this.font = `${styles.fontSize} monospace`;
}
public measure(text: string): TextMetrics {
if (!this.#ctx) {
throw new TypeError("CanvasRenderingContext2D is null");
}
return this.#ctx.measureText(text);
}
public [Symbol.dispose](): void {
this.#ctx = null;
}
}

View File

@@ -38,6 +38,7 @@
font-size: var(--icon-font-size, var(--icon-height));
color: var(--ak-global--Color--100);
padding: var(--icon-border);
height: var(--icon-height);
max-height: calc(var(--icon-height) + var(--icon-border) + var(--icon-border));
line-height: 1;
filter: drop-shadow(-0.5px 0px 0px var(--app-icon-shadow-blend-color));

View File

@@ -28,6 +28,7 @@ export interface AKLibraryAppProps extends HTMLAttributes<HTMLDivElement> {
application?: Application;
editURL?: string | URL | null;
background?: string | null;
titleWidth?: number;
appIndex: number;
groupIndex: number;
targetRef?: RefOrCallback | null;
@@ -37,6 +38,7 @@ export const AKLibraryApp: LitFC<AKLibraryAppProps> = ({
application,
editURL,
background,
titleWidth,
appIndex,
groupIndex,
className = "",
@@ -83,7 +85,10 @@ export const AKLibraryApp: LitFC<AKLibraryAppProps> = ({
part="card-wrapper"
data-application-name=${ifPresent(dataID)}
aria-describedby=${descriptionID}
style=${styleMap({ background: background || null })}
style=${styleMap({
"background": background || null,
"--ak-card-title-width": titleWidth ? `${titleWidth}px` : null,
})}
${spread(props)}
>
<div part="card" class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}">

View File

@@ -5,11 +5,16 @@
--app-card-min-width: 6.5rem;
--app-group-header-min-height: calc(var(--app-card-min-width) / 4);
--app-icon-offset: 1rem;
--app-card-row-coefficient: 3.5;
--app-card-row-coefficient: 2.5;
--app-card-font-size-minimum: var(--pf-global--FontSize--md);
--app-card-font-size-maximum: var(--pf-global--FontSize--md);
--app-card-line-clamp: 3;
@media (min-width: 390px) {
--app-card-row-coefficient: 3.5;
--app-card-aspect-ratio: 1;
--app-group-template-columns: repeat(auto-fill, var(--app-card-min-width));
--app-card-font-size-minimum: 0.6rem;
}
@media (min-width: 409px) {
@@ -19,6 +24,8 @@
@media (min-width: 768px) {
--app-icon-offset: 0.5rem;
--app-card-min-width: 10rem;
--app-card-font-size-minimum: 0.9rem;
--app-card-line-clamp: 3;
}
}
@@ -76,6 +83,15 @@
/* #region Title */
[part="card-title"] {
--font-size-adjustment: calc(
(var(--app-card-min-width) / var(--ak-card-title-width, var(--app-card-min-width)))
);
--pf-c-card__title--FontSize: clamp(
var(--app-card-font-size-minimum),
calc(1rem * var(--font-size-adjustment, 1)),
var(--app-card-font-size-maximum)
);
padding: 0 !important;
z-index: 1;
text-stroke-width: 0.15em;
@@ -93,16 +109,16 @@
.clamp-wrapper {
--clamp-padding: calc(0.1em * var(--app-card-row-coefficient));
display: box;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
line-clamp: var(--app-card-line-clamp);
-webkit-line-clamp: var(--app-card-line-clamp);
box-orient: vertical;
-webkit-box-orient: vertical;
overflow: hidden;
text-align: center;
text-wrap: balance;
word-break: break-word;
line-height: 1.2;
padding-block: var(--clamp-padding);
max-height: calc((var(--pf-global--LineHeight--md) * 2rem) - (var(--clamp-padding) / 2));

View File

@@ -30,6 +30,7 @@ const LayoutColumnCount = {
} as const satisfies Record<LayoutType, number>;
export interface AKLibraryApplicationListProps extends HTMLAttributes<HTMLDivElement> {
measurements?: WeakMap<Application, number>;
groupedApps: AppGroupEntry[];
layout: LayoutType;
background?: string | null;
@@ -46,6 +47,7 @@ export const AKLibraryApplicationList: LitFC<AKLibraryApplicationListProps> = ({
background,
selectedApp,
targetRef,
measurements,
...props
}) => {
const columnCount = LayoutColumnCount[layout] ?? 1;
@@ -84,6 +86,7 @@ export const AKLibraryApplicationList: LitFC<AKLibraryApplicationListProps> = ({
apps,
(application) => application.pk,
(application, appIndex) => {
const titleWidth = measurements?.get(application);
const selected = selectedApp === application;
const editURL = canEdit
@@ -96,6 +99,7 @@ export const AKLibraryApplicationList: LitFC<AKLibraryApplicationListProps> = ({
groupIndex,
background,
editURL,
titleWidth,
"targetRef": selected ? targetRef : null,
"aria-selected": selected,
});

View File

@@ -8,6 +8,7 @@ import { AKLibraryApplicationList } from "./ApplicationList.js";
import { appHasLaunchUrl } from "./LibraryPageImpl.utils.js";
import type { PageUIConfig } from "./types.js";
import { TextMeasurer } from "#common/text/measurement";
import { groupBy } from "#common/utils";
import { AKSkipToContent } from "#elements/a11y/ak-skip-to-content";
@@ -82,6 +83,9 @@ export class LibraryPage extends AKElement {
@property({ type: Boolean })
public admin = false;
#textMeasurer = new TextMeasurer();
#titleMeasurements = new WeakMap<Application, number>();
#applications: Application[] = [];
/**
@@ -98,6 +102,12 @@ export class LibraryPage extends AKElement {
this.#applications = value;
this.fuse.setCollection(this.searchEnabled ? this.#applications : []);
for (const app of this.#applications) {
const metrics = this.#textMeasurer.measure(app.name);
const titleWidth = Math.ceil(metrics.width);
this.#titleMeasurements.set(app, titleWidth);
}
}
/**
@@ -296,6 +306,7 @@ export class LibraryPage extends AKElement {
background,
selectedApp,
groupedApps,
measurements: this.#titleMeasurements,
targetRef: this.targetRef,
});
}