typescript

This commit is contained in:
2025-11-13 01:07:55 +01:00
parent b812d748ea
commit ada089c05b
147 changed files with 7427 additions and 2534 deletions
@@ -1,10 +1,10 @@
const HEX_COLOR_PATTERN = /^#?([0-9a-fA-F]{6})$/;
const clamp01 = (value) => Math.min(1, Math.max(0, value));
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
const clampRange = (value, min, max) => Math.min(max, Math.max(min, value));
const clampRange = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));
const hslToHex = (h, s, l) => {
const hslToHex = (h: number, s: number, l: number): string => {
const normalizedH = ((h % 360) + 360) % 360;
const sat = clamp01(s);
const light = clamp01(l);
@@ -48,7 +48,13 @@ const hslToHex = (h, s, l) => {
return `#${((sr << 16) | (sg << 8) | sb).toString(16).padStart(6, '0')}`;
};
const rgbToHsl = ({ r, g, b }) => {
interface RgbColor {
r: number;
g: number;
b: number;
}
const rgbToHsl = ({ r, g, b }: RgbColor) => {
const rn = r / 255;
const gn = g / 255;
const bn = b / 255;
@@ -79,7 +85,7 @@ const rgbToHsl = ({ r, g, b }) => {
return { h: hue, s: clamp01(saturation), l: clamp01(lightness) };
};
export const hexToRgb = (input) => {
export const hexToRgb = (input: string | null | undefined): (RgbColor & { hex: string }) | null => {
if (!input) return null;
const match = HEX_COLOR_PATTERN.exec(input.trim());
if (!match) return null;
@@ -92,7 +98,7 @@ export const hexToRgb = (input) => {
};
};
export const relativeLuminance = ({ r, g, b }) => {
export const relativeLuminance = ({ r, g, b }: RgbColor): number => {
const toLinear = (channel) => {
const normalized = channel / 255;
return normalized <= 0.03928
@@ -104,12 +110,12 @@ export const relativeLuminance = ({ r, g, b }) => {
return 0.2126 * red + 0.7152 * green + 0.0722 * blue;
};
const contrastRatio = (lumA, lumB) => {
const contrastRatio = (lumA: number, lumB: number): number => {
const [lighter, darker] = lumA >= lumB ? [lumA, lumB] : [lumB, lumA];
return (lighter + 0.05) / (darker + 0.05);
};
const parseCandidateColor = (candidate) => {
const parseCandidateColor = (candidate: string) => {
const rgb = hexToRgb(candidate);
if (!rgb) return null;
return {
@@ -118,7 +124,7 @@ const parseCandidateColor = (candidate) => {
};
};
const contrastForPair = (backgroundHex, textHex) => {
const contrastForPair = (backgroundHex: string, textHex: string): number => {
const background = hexToRgb(backgroundHex);
const text = hexToRgb(textHex);
if (!background || !text) {
@@ -128,9 +134,9 @@ const contrastForPair = (backgroundHex, textHex) => {
};
export const getReadableTextColor = (
hex,
{ light = '#1f1f1f', dark = '#ffffff', fallback = '#1f1f1f' } = {},
) => {
hex: string,
{ light = '#1f1f1f', dark = '#ffffff', fallback = '#1f1f1f' }: { light?: string; dark?: string; fallback?: string } = {},
): string => {
const background = hexToRgb(hex);
if (!background) return fallback;
@@ -1,4 +1,4 @@
const ensureDate = (value) => {
const ensureDate = (value: string | number | Date | null | undefined): Date | null => {
if (!value) {
return null;
}
@@ -6,7 +6,13 @@ const ensureDate = (value) => {
return Number.isNaN(date.getTime()) ? null : date;
};
export const formatDate = (value, { fallback = '—', locale, options } = {}) => {
interface FormatOptions {
fallback?: string;
locale?: Intl.LocalesArgument;
options?: Intl.DateTimeFormatOptions;
}
export const formatDate = (value: string | number | Date | null | undefined, { fallback = '—', locale, options }: FormatOptions = {}): string => {
const date = ensureDate(value);
if (!date) {
return fallback;
@@ -14,7 +20,7 @@ export const formatDate = (value, { fallback = '—', locale, options } = {}) =>
return date.toLocaleDateString(locale, options);
};
export const formatDateTime = (value, { fallback = '—', locale, options } = {}) => {
export const formatDateTime = (value: string | number | Date | null | undefined, { fallback = '—', locale, options }: FormatOptions = {}): string => {
const date = ensureDate(value);
if (!date) {
return fallback;
@@ -22,7 +28,7 @@ export const formatDateTime = (value, { fallback = '—', locale, options } = {}
return date.toLocaleString(locale, options);
};
export const toDateInputValue = (value) => {
export const toDateInputValue = (value: string | number | Date | null | undefined): string => {
const date = ensureDate(value);
if (!date) {
return '';
@@ -32,7 +38,7 @@ export const toDateInputValue = (value) => {
return localDate.toISOString().slice(0, 10);
};
export const toIssuedTimestamp = (dateString, fallback) => {
export const toIssuedTimestamp = (dateString: string | null | undefined, fallback: string | number | Date | null | undefined): string | null => {
if (!dateString) {
return null;
}
@@ -46,7 +52,7 @@ export const toIssuedTimestamp = (dateString, fallback) => {
return Number.isNaN(candidate.getTime()) ? null : candidate.toISOString();
};
export const parseDateValue = (value) => ensureDate(value);
export const parseDateValue = (value: string | number | Date | null | undefined): Date | null => ensureDate(value);
export default {
formatDate,
@@ -1,4 +1,4 @@
export const formatFileSize = (value) => {
export const formatFileSize = (value: number | string): string => {
const bytes = Number(value);
if (!Number.isFinite(bytes) || bytes <= 0) {
return '0 B';
@@ -15,4 +15,3 @@ export const formatFileSize = (value) => {
return `${amount.toFixed(2)} ${units[index]}`;
};
@@ -1,4 +1,4 @@
export const clamp = (value, min, max) => {
export const clamp = (value: number, min: number, max: number): number => {
if (value < min) {
return min;
}
@@ -1,9 +1,34 @@
import { createAssetView, resolveDocumentAssetUrl } from '../asset_manager';
const BASE_FETCH_OPTIONS = { start: 1, limit: 1 };
const BASE_FETCH_OPTIONS = { start: 1, limit: 1 } as const;
const pickAsset = (doc, getDocumentAsset) => {
if (!doc || typeof getDocumentAsset !== 'function') {
interface DocumentLike {
id?: string | number;
current_version?: unknown;
}
interface AssetLike {
id?: string | number;
[key: string]: unknown;
}
type EnsurePreviewData = (id: string | number) => Promise<DocumentLike | null | undefined>;
type GetDocumentAsset = (doc: DocumentLike, type: string) => AssetLike | null | undefined;
type EnsureAssetUrl = (
id: string | number,
asset: AssetLike,
options?: { start?: number; limit?: number; force?: boolean },
) => Promise<AssetLike | null | undefined>;
interface ResolveOcrTextUrlOptions {
document: DocumentLike | null;
ensurePreviewData?: EnsurePreviewData;
getDocumentAsset?: GetDocumentAsset;
ensureAssetUrl?: EnsureAssetUrl;
}
const pickAsset = (doc: DocumentLike | null | undefined, getDocumentAsset?: GetDocumentAsset): AssetLike | null => {
if (!doc || !getDocumentAsset) {
return null;
}
return getDocumentAsset(doc, 'ocr-text') || null;
@@ -14,7 +39,7 @@ export async function resolveOcrTextUrl({
ensurePreviewData,
getDocumentAsset,
ensureAssetUrl,
}) {
}: ResolveOcrTextUrlOptions): Promise<string | null> {
if (!document?.id) {
return null;
}
@@ -22,7 +47,7 @@ export async function resolveOcrTextUrl({
let docRef = document;
let asset = pickAsset(docRef, getDocumentAsset);
if (!asset && typeof ensurePreviewData === 'function') {
if (!asset && ensurePreviewData) {
const refreshed = await ensurePreviewData(docRef.id);
if (refreshed) {
docRef = refreshed;
@@ -38,9 +63,9 @@ export async function resolveOcrTextUrl({
const hasUrl = Boolean(baseView.getPrimaryUrl());
let entry = asset;
if (typeof ensureAssetUrl === 'function') {
if (ensureAssetUrl) {
const ensureOptions = { ...BASE_FETCH_OPTIONS, force: !hasUrl };
entry = (await ensureAssetUrl(docRef.id, asset, ensureOptions)) || asset;
entry = (await ensureAssetUrl(docRef.id!, asset, ensureOptions)) || asset;
}
const ensuredView = createAssetView(entry);
@@ -59,7 +84,7 @@ export async function resolveOcrTextUrl({
);
}
export async function openOcrTextInNewTab(options) {
export async function openOcrTextInNewTab(options: ResolveOcrTextUrlOptions): Promise<boolean> {
const popup = window.open('', '_blank');
const popupAvailable = Boolean(popup);
-151
View File
@@ -1,151 +0,0 @@
const base64urlToBase64 = (value = '') => {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
const padding = normalized.length % 4;
if (padding === 0) {
return normalized;
}
const padLength = 4 - padding;
return normalized + '='.repeat(padLength);
};
const base64ToBase64url = (value = '') =>
value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
const decodeBase64 = (value) => {
if (window.atob) {
return window.atob(value);
}
const bufferCtor = globalThis?.Buffer;
if (bufferCtor) {
return bufferCtor.from(value, 'base64').toString('binary');
}
throw new Error('No base64 decoder available.');
};
const encodeBase64 = (binary) => {
if (window.btoa) {
return window.btoa(binary);
}
const bufferCtor = globalThis?.Buffer;
if (bufferCtor) {
return bufferCtor.from(binary, 'binary').toString('base64');
}
throw new Error('No base64 encoder available.');
};
export const base64urlToUint8Array = (value) => {
const base64 = base64urlToBase64(value || '');
const binary = decodeBase64(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
};
export const arrayBufferToBase64url = (buffer) => {
const bytes = new Uint8Array(buffer || []);
let binary = '';
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = encodeBase64(binary);
return base64ToBase64url(base64);
};
export const isWebAuthnAvailable = () =>
Boolean(navigator?.credentials?.create && navigator.credentials.get);
export const preparePublicKeyCreationOptions = (challengeResponse) => {
if (!challengeResponse || !challengeResponse.publicKey) {
throw new Error('Missing publicKey challenge options.');
}
const publicKey = { ...challengeResponse.publicKey };
if (publicKey.challenge) {
publicKey.challenge = base64urlToUint8Array(publicKey.challenge);
}
if (publicKey.user?.id) {
publicKey.user = {
...publicKey.user,
id: base64urlToUint8Array(publicKey.user.id),
};
}
if (Array.isArray(publicKey.excludeCredentials)) {
publicKey.excludeCredentials = publicKey.excludeCredentials.map((descriptor) => ({
...descriptor,
id: base64urlToUint8Array(descriptor.id),
}));
}
if (publicKey.authenticatorSelection?.residentKey === 'discouraged' && !publicKey.authenticatorSelection.requireResidentKey) {
delete publicKey.authenticatorSelection.requireResidentKey;
}
return publicKey;
};
export const preparePublicKeyRequestOptions = (challengeResponse) => {
if (!challengeResponse || !challengeResponse.publicKey) {
throw new Error('Missing publicKey request options.');
}
const publicKey = { ...challengeResponse.publicKey };
if (publicKey.challenge) {
publicKey.challenge = base64urlToUint8Array(publicKey.challenge);
}
if (Array.isArray(publicKey.allowCredentials)) {
publicKey.allowCredentials = publicKey.allowCredentials.map((descriptor) => ({
...descriptor,
id: base64urlToUint8Array(descriptor.id),
}));
}
return publicKey;
};
export const serializeRegistrationCredential = (credential) => {
if (!credential) {
return null;
}
const transports = credential?.response?.getTransports?.();
return {
id: credential.id,
type: credential.type,
rawId: arrayBufferToBase64url(credential.rawId),
response: {
clientDataJSON: arrayBufferToBase64url(credential.response.clientDataJSON),
attestationObject: arrayBufferToBase64url(credential.response.attestationObject),
transports: transports && transports.length ? Array.from(transports) : undefined,
},
clientExtensionResults: credential.getClientExtensionResults?.() || {},
};
};
export const serializeAuthenticationCredential = (credential) => {
if (!credential) {
return null;
}
return {
id: credential.id,
type: credential.type,
rawId: arrayBufferToBase64url(credential.rawId),
response: {
clientDataJSON: arrayBufferToBase64url(credential.response.clientDataJSON),
authenticatorData: arrayBufferToBase64url(credential.response.authenticatorData),
signature: arrayBufferToBase64url(credential.response.signature),
userHandle: credential.response.userHandle
? arrayBufferToBase64url(credential.response.userHandle)
: undefined,
},
clientExtensionResults: credential.getClientExtensionResults?.() || {},
};
};
+249
View File
@@ -0,0 +1,249 @@
/* global PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, BufferSource, PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor, AuthenticatorSelectionCriteria, AuthenticatorTransport, AuthenticationExtensionsClientOutputs */
type BufferCtor = {
from: (input: string, encoding: string) => {
toString: (encoding: string) => string;
};
};
type CreationChallengeResponse = {
publicKey?: PublicKeyCredentialCreationOptions & {
challenge?: string | BufferSource;
user?: PublicKeyCredentialUserEntity & { id?: string | BufferSource };
excludeCredentials?: Array<PublicKeyCredentialDescriptor & { id?: string | BufferSource }>;
authenticatorSelection?: AuthenticatorSelectionCriteria & { requireResidentKey?: boolean };
};
};
type RequestChallengeResponse = {
publicKey?: PublicKeyCredentialRequestOptions & {
challenge?: string | BufferSource;
allowCredentials?: Array<PublicKeyCredentialDescriptor & { id?: string | BufferSource }>;
};
};
type RegistrationCredential = PublicKeyCredential & {
response: AuthenticatorAttestationResponse & {
getTransports?: () => AuthenticatorTransport[];
};
};
type AuthenticationCredential = PublicKeyCredential & {
response: AuthenticatorAssertionResponse;
};
const base64urlToBase64 = (value: string = ''): string => {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
const padding = normalized.length % 4;
if (padding === 0) {
return normalized;
}
const padLength = 4 - padding;
return normalized + '='.repeat(padLength);
};
const base64ToBase64url = (value: string = ''): string =>
value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
const getWindowObject = (): (Window & typeof globalThis) | null =>
(typeof window !== 'undefined' ? window : null);
const decodeBase64 = (value: string): string => {
const win = getWindowObject();
if (win?.atob) {
return win.atob(value);
}
const bufferCtor = (globalThis as typeof globalThis & { Buffer?: BufferCtor }).Buffer;
if (bufferCtor) {
return bufferCtor.from(value, 'base64').toString('binary');
}
throw new Error('No base64 decoder available.');
};
const encodeBase64 = (binary: string): string => {
const win = getWindowObject();
if (win?.btoa) {
return win.btoa(binary);
}
const bufferCtor = (globalThis as typeof globalThis & { Buffer?: BufferCtor }).Buffer;
if (bufferCtor) {
return bufferCtor.from(binary, 'binary').toString('base64');
}
throw new Error('No base64 encoder available.');
};
export const base64urlToUint8Array = (value?: string | null): Uint8Array => {
const base64 = base64urlToBase64(value || '');
const binary = decodeBase64(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
};
const toUint8Array = (input?: ArrayBuffer | ArrayBufferView | ArrayLike<number> | null): Uint8Array => {
if (!input) {
return new Uint8Array();
}
if (input instanceof ArrayBuffer) {
return new Uint8Array(input as ArrayBuffer) as Uint8Array;
}
if (ArrayBuffer.isView(input)) {
const buffer = (input.buffer as ArrayBuffer).slice(
input.byteOffset,
input.byteOffset + input.byteLength,
);
return new Uint8Array(buffer) as Uint8Array;
}
return Uint8Array.from(input as ArrayLike<number>) as Uint8Array;
};
export const arrayBufferToBase64url = (
buffer?: ArrayBuffer | ArrayBufferView | ArrayLike<number> | null,
): string => {
const bytes = toUint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.length; i += 1) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = encodeBase64(binary);
return base64ToBase64url(base64);
};
export const isWebAuthnAvailable = (): boolean =>
Boolean(typeof navigator !== 'undefined' && navigator?.credentials?.create && navigator.credentials.get);
export const preparePublicKeyCreationOptions = (
challengeResponse: CreationChallengeResponse,
): PublicKeyCredentialCreationOptions => {
if (!challengeResponse || !challengeResponse.publicKey) {
throw new Error('Missing publicKey challenge options.');
}
const publicKey: PublicKeyCredentialCreationOptions = { ...challengeResponse.publicKey };
if (publicKey.challenge && typeof publicKey.challenge === 'string') {
publicKey.challenge = base64urlToUint8Array(publicKey.challenge);
}
if (publicKey.user?.id) {
publicKey.user = {
...publicKey.user,
id: typeof publicKey.user.id === 'string'
? base64urlToUint8Array(publicKey.user.id)
: publicKey.user.id,
};
}
if (Array.isArray(publicKey.excludeCredentials)) {
publicKey.excludeCredentials = publicKey.excludeCredentials.map((descriptor) => ({
...descriptor,
id: typeof descriptor.id === 'string'
? base64urlToUint8Array(descriptor.id)
: descriptor.id,
}));
}
if (publicKey.authenticatorSelection?.residentKey === 'discouraged' && !publicKey.authenticatorSelection.requireResidentKey) {
delete publicKey.authenticatorSelection.requireResidentKey;
}
return publicKey;
};
export const preparePublicKeyRequestOptions = (
challengeResponse: RequestChallengeResponse,
): PublicKeyCredentialRequestOptions => {
if (!challengeResponse || !challengeResponse.publicKey) {
throw new Error('Missing publicKey request options.');
}
const publicKey: PublicKeyCredentialRequestOptions = { ...challengeResponse.publicKey };
if (publicKey.challenge && typeof publicKey.challenge === 'string') {
publicKey.challenge = base64urlToUint8Array(publicKey.challenge);
}
if (Array.isArray(publicKey.allowCredentials)) {
publicKey.allowCredentials = publicKey.allowCredentials.map((descriptor) => ({
...descriptor,
id: typeof descriptor.id === 'string'
? base64urlToUint8Array(descriptor.id)
: descriptor.id,
}));
}
return publicKey;
};
export const serializeRegistrationCredential = (
credential?: PublicKeyCredential | null,
): {
id: string;
type: PublicKeyCredential['type'];
rawId: string;
response: {
clientDataJSON: string;
attestationObject: string;
transports?: AuthenticatorTransport[];
};
clientExtensionResults: AuthenticationExtensionsClientOutputs;
} | null => {
if (!credential) {
return null;
}
const response = credential.response as RegistrationCredential['response'];
const transports = response?.getTransports?.();
return {
id: credential.id,
type: credential.type,
rawId: arrayBufferToBase64url(credential.rawId),
response: {
clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
attestationObject: arrayBufferToBase64url(response.attestationObject),
transports: transports && transports.length
? (Array.from(transports) as AuthenticatorTransport[])
: undefined,
},
clientExtensionResults: credential.getClientExtensionResults?.() || {},
};
};
export const serializeAuthenticationCredential = (
credential?: PublicKeyCredential | null,
): {
id: string;
type: PublicKeyCredential['type'];
rawId: string;
response: {
clientDataJSON: string;
authenticatorData: string;
signature: string;
userHandle?: string;
};
clientExtensionResults: AuthenticationExtensionsClientOutputs;
} | null => {
if (!credential) {
return null;
}
const response = credential.response as AuthenticationCredential['response'];
return {
id: credential.id,
type: credential.type,
rawId: arrayBufferToBase64url(credential.rawId),
response: {
clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
authenticatorData: arrayBufferToBase64url(response.authenticatorData),
signature: arrayBufferToBase64url(response.signature),
userHandle: response.userHandle
? arrayBufferToBase64url(response.userHandle)
: undefined,
},
clientExtensionResults: credential.getClientExtensionResults?.() || {},
};
};