mirror of
https://github.com/goauthentik/authentik
synced 2026-05-15 11:26:31 +02:00
Compare commits
1 Commits
website/do
...
improve-ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0135914c93 |
47
web/src/common/text/measurement.ts
Normal file
47
web/src/common/text/measurement.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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)}">
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user