diff --git a/src/common/utils/accessibility-preferences.ts b/src/common/utils/accessibility-preferences.ts new file mode 100644 index 00000000..a7eaaea2 --- /dev/null +++ b/src/common/utils/accessibility-preferences.ts @@ -0,0 +1,30 @@ +/** + * Validate and apply player accessibility preferences. (#419) + */ +export interface AccessibilityPreferences { + highContrast: boolean; + textToSpeech: boolean; + fontSizeScale: number; + colorBlindMode: "none" | "protanopia" | "deuteranopia" | "tritanopia"; +} + +export const DEFAULT_ACCESSIBILITY_PREFERENCES: AccessibilityPreferences = { + highContrast: false, + textToSpeech: false, + fontSizeScale: 1, + colorBlindMode: "none", +}; + +export function validateFontSizeScale(scale: number): boolean { + return scale >= 0.75 && scale <= 2.0; +} + +export function mergeAccessibilityPreferences( + overrides: Partial, +): AccessibilityPreferences { + const merged = { ...DEFAULT_ACCESSIBILITY_PREFERENCES, ...overrides }; + if (!validateFontSizeScale(merged.fontSizeScale)) { + merged.fontSizeScale = DEFAULT_ACCESSIBILITY_PREFERENCES.fontSizeScale; + } + return merged; +} diff --git a/src/common/utils/bandwidth-adaptive-payload.ts b/src/common/utils/bandwidth-adaptive-payload.ts new file mode 100644 index 00000000..9003ff7d --- /dev/null +++ b/src/common/utils/bandwidth-adaptive-payload.ts @@ -0,0 +1,21 @@ +/** + * Select a mobile-appropriate response payload variant based on the + * client's reported network condition. (#420) + */ +export type NetworkType = "wifi" | "4g" | "3g" | "2g" | "offline"; +export type PayloadVariant = "full" | "reduced" | "minimal"; + +export function selectPayloadVariant(network: NetworkType): PayloadVariant { + if (network === "wifi" || network === "4g") return "full"; + if (network === "3g") return "reduced"; + return "minimal"; +} + +export function shouldDeferSync(network: NetworkType): boolean { + return network === "offline" || network === "2g"; +} + +export function estimatedPayloadSizeKb(variant: PayloadVariant, baseSizeKb: number): number { + const factor = variant === "full" ? 1 : variant === "reduced" ? 0.5 : 0.2; + return Math.round(baseSizeKb * factor); +} diff --git a/src/common/utils/search-relevance-scorer.ts b/src/common/utils/search-relevance-scorer.ts new file mode 100644 index 00000000..e457db99 --- /dev/null +++ b/src/common/utils/search-relevance-scorer.ts @@ -0,0 +1,34 @@ +/** + * Score search result relevance using term-frequency overlap, for the + * advanced search and discovery feature. (#421) + */ +export interface SearchableDocument { + id: string; + title: string; + tags: string[]; +} + +function tokenize(text: string): string[] { + return text.toLowerCase().split(/\W+/).filter(Boolean); +} + +export function scoreSearchResult(query: string, doc: SearchableDocument): number { + const queryTerms = tokenize(query); + const titleTerms = tokenize(doc.title); + const tagTerms = doc.tags.map((tag) => tag.toLowerCase()); + + let score = 0; + for (const term of queryTerms) { + if (titleTerms.includes(term)) score += 2; + if (tagTerms.includes(term)) score += 1; + } + return score; +} + +export function searchAndRank(query: string, documents: SearchableDocument[]): SearchableDocument[] { + return documents + .map((doc) => ({ doc, score: scoreSearchResult(query, doc) })) + .filter((entry) => entry.score > 0) + .sort((a, b) => b.score - a.score) + .map((entry) => entry.doc); +} diff --git a/src/common/utils/test-data-factory.ts b/src/common/utils/test-data-factory.ts new file mode 100644 index 00000000..4f9db127 --- /dev/null +++ b/src/common/utils/test-data-factory.ts @@ -0,0 +1,22 @@ +/** + * Generic test data factory to reduce fixture boilerplate. (#416) + */ +export type FactoryDefaults = () => T; +export type FactoryOverrides = Partial | ((base: T) => Partial); + +export function createFactory(defaults: FactoryDefaults) { + return function build(overrides: FactoryOverrides = {}): T { + const base = defaults(); + const patch = typeof overrides === "function" ? overrides(base) : overrides; + return { ...base, ...patch }; + }; +} + +export function buildMany(build: (overrides?: FactoryOverrides) => T, count: number): T[] { + return Array.from({ length: count }, () => build()); +} + +export function sequence(prefix: string): () => string { + let counter = 0; + return () => `${prefix}-${++counter}`; +}