UNPKG

tfl-ts

Version:

🚇 Fully-typed TypeScript client for Transport for London (TfL) API • Zero dependencies • Auto-generated types • Real-time arrivals • Journey planning • Universal compatibility

530 lines (529 loc) • 17.5 kB
"use strict"; /** * UI Utilities for TfL API Wrapper * * This module provides utilities for building user interfaces with TfL data, * including line colors, severity helpers, accessibility utilities, and more. * * @example * import { getLineColor, getSeverityCategory, getAccessibleSeverityLabel } from 'tfl-ts/utils/ui'; * * // Get line colors for styling * const colors = getLineColor('central'); // { hex: '#E32017', text: 'text-[#E32017]', bg: 'bg-[#E32017]' } * * // Get severity category for conditional styling * const category = getSeverityCategory(6); // 'severe' * * // Get accessible label for screen readers * const label = getAccessibleSeverityLabel(10, 'Good Service'); // 'Good Service - No issues reported' */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getLineStatusSummary = exports.getLineDisplayName = exports.sortLinesBySeverityAndOrder = exports.getLineCssProps = exports.getLineAriaLabel = exports.hasNightService = exports.isNormalService = exports.getLineOrder = exports.getAccessibleSeverityLabel = exports.getSeverityClasses = exports.getSeverityCategory = exports.getLineColor = exports.SEVERITY_MAPPING = exports.buildSeverityMapping = exports.LINE_ORDER = exports.DEFAULT_LINE_COLOR = exports.LINE_COLORS = void 0; const Meta_1 = require("../generated/meta/Meta"); /** * Official TfL line colors with accessibility considerations * * Colors are based on TfL's official brand guidelines and include * accessibility considerations for dark mode and contrast ratios. */ exports.LINE_COLORS = { // Tube lines 'bakerloo': { hex: '#B36305', text: 'text-[#B36305]', bg: 'bg-[#B36305]', poorDarkContrast: false }, 'central': { hex: '#E32017', text: 'text-[#E32017]', bg: 'bg-[#E32017]', poorDarkContrast: false }, 'circle': { hex: '#FFD300', text: 'text-[#FFD300]', bg: 'bg-[#FFD300]', poorDarkContrast: false }, 'district': { hex: '#00782A', text: 'text-[#00782A]', bg: 'bg-[#00782A]', poorDarkContrast: false }, 'hammersmith-city': { hex: '#F3A9BB', text: 'text-[#F3A9BB]', bg: 'bg-[#F3A9BB]', poorDarkContrast: false }, 'jubilee': { hex: '#A0A5A9', text: 'text-[#A0A5A9]', bg: 'bg-[#A0A5A9]', poorDarkContrast: false }, 'metropolitan': { hex: '#9B0056', text: 'text-[#9B0056]', bg: 'bg-[#9B0056]', poorDarkContrast: false }, 'northern': { hex: '#000000', text: 'text-[#000000]', bg: 'bg-[#000000]', poorDarkContrast: true, darkText: 'text-white', darkBg: 'bg-white' }, 'piccadilly': { hex: '#003688', text: 'text-[#003688]', bg: 'bg-[#003688]', poorDarkContrast: false }, 'victoria': { hex: '#0098D4', text: 'text-[#0098D4]', bg: 'bg-[#0098D4]', poorDarkContrast: false }, 'waterloo-city': { hex: '#95CDBA', text: 'text-[#95CDBA]', bg: 'bg-[#95CDBA]', poorDarkContrast: false }, // Other transport modes 'dlr': { hex: '#00A4A7', text: 'text-[#00A4A7]', bg: 'bg-[#00A4A7]', poorDarkContrast: false }, 'elizabeth': { hex: '#6950A1', text: 'text-[#6950A1]', bg: 'bg-[#6950A1]', poorDarkContrast: false }, 'tram': { hex: '#5fb526', text: 'text-[#5fb526]', bg: 'bg-[#5fb526]', poorDarkContrast: false }, // Overground lines 'liberty': { hex: '#0071FD', text: 'text-[#0071FD]', bg: 'bg-[#0071FD]', poorDarkContrast: false }, 'lioness': { hex: '#FC9D9A', text: 'text-[#FC9D9A]', bg: 'bg-[#FC9D9A]', poorDarkContrast: false }, 'mildmay': { hex: '#0071FD', text: 'text-[#0071FD]', bg: 'bg-[#0071FD]', poorDarkContrast: false }, 'suffragette': { hex: '#76B82A', text: 'text-[#76B82A]', bg: 'bg-[#76B82A]', poorDarkContrast: false }, 'weaver': { hex: '#A45A2A', text: 'text-[#A45A2A]', bg: 'bg-[#A45A2A]', poorDarkContrast: false }, 'windrush': { hex: '#EE2E24', text: 'text-[#EE2E24]', bg: 'bg-[#EE2E24]', poorDarkContrast: false } }; /** * Default color for unknown lines */ exports.DEFAULT_LINE_COLOR = { hex: '#6B7280', text: 'text-gray-500', bg: 'bg-gray-500', poorDarkContrast: false }; /** * Line ordering by passenger volume and importance * Used for consistent sorting in UI displays */ exports.LINE_ORDER = [ // Tube lines by passenger volume (busiest first) 'central', 'northern', 'jubilee', 'piccadilly', 'district', 'victoria', 'circle', 'hammersmith-city', 'bakerloo', 'metropolitan', 'waterloo-city', // Other transport modes 'dlr', 'tram', 'elizabeth', // Overground group 'liberty', 'lioness', 'mildmay', 'suffragette', 'weaver', 'windrush' ]; /** * Build severity mapping from generated TfL data * * This creates a smart categorization of severity levels based on * the actual descriptions from the TfL API, making it easier to * apply consistent styling across different transport modes. */ const buildSeverityMapping = () => { const mapping = { critical: [], severe: [], minor: [], special: [], good: [] }; Meta_1.Severity.forEach(item => { const level = item.severityLevel; const description = item.description.toLowerCase(); if (description.includes('closed') || description.includes('suspended') || description.includes('not running')) { mapping.critical.push(level); } else if (description.includes('severe') || description.includes('part closure') || description.includes('exit only')) { mapping.severe.push(level); } else if (description.includes('minor') || description.includes('reduced') || description.includes('bus service') || description.includes('diverted') || description.includes('issues')) { mapping.minor.push(level); } else if (description.includes('special') || description.includes('no step free') || description.includes('information')) { mapping.special.push(level); } else if (description.includes('good') || description.includes('no issues') || description.includes('closed for the night')) { mapping.good.push(level); } }); return mapping; }; exports.buildSeverityMapping = buildSeverityMapping; /** * Pre-built severity mapping for immediate use */ exports.SEVERITY_MAPPING = (0, exports.buildSeverityMapping)(); /** * Get line color information * * @param lineId - The line ID to get colors for * @returns LineColorInfo object with hex, Tailwind classes, and accessibility info * * @example * const colors = getLineColor('central'); * // Returns: { hex: '#E32017', text: 'text-[#E32017]', bg: 'bg-[#E32017]', poorDarkContrast: false } */ const getLineColor = (lineId) => { return exports.LINE_COLORS[lineId] || exports.DEFAULT_LINE_COLOR; }; exports.getLineColor = getLineColor; /** * Get severity category for a severity level * * @param severityLevel - The severity level number * @returns SeverityCategory for conditional styling * * @example * const category = getSeverityCategory(6); // 'severe' * const category = getSeverityCategory(10); // 'good' */ const getSeverityCategory = (severityLevel) => { if (exports.SEVERITY_MAPPING.critical.includes(severityLevel)) return 'critical'; if (exports.SEVERITY_MAPPING.severe.includes(severityLevel)) return 'severe'; if (exports.SEVERITY_MAPPING.minor.includes(severityLevel)) return 'minor'; if (exports.SEVERITY_MAPPING.special.includes(severityLevel)) return 'special'; if (exports.SEVERITY_MAPPING.good.includes(severityLevel)) return 'good'; // Default to minor for unknown severity levels return 'minor'; }; exports.getSeverityCategory = getSeverityCategory; /** * Get Tailwind CSS classes for severity styling * * @param severityLevel - The severity level number * @param includeAnimation - Whether to include animation classes * @returns Object with text and animation classes * * @example * const classes = getSeverityClasses(6, true); * // Returns: { text: 'text-orange-700', animation: 'animate-[pulse_1.5s_ease-in-out_infinite]' } */ const getSeverityClasses = (severityLevel, includeAnimation = false) => { const category = (0, exports.getSeverityCategory)(severityLevel); const textClasses = { critical: 'text-red-700', severe: 'text-orange-700', minor: 'text-yellow-700', special: 'text-blue-700', good: 'text-green-700' }; const animationClasses = { critical: 'animate-[pulse_1s_ease-in-out_infinite]', severe: 'animate-[pulse_1.5s_ease-in-out_infinite]', minor: 'animate-[pulse_2s_ease-in-out_infinite]', special: '', good: '' }; return { text: textClasses[category], animation: includeAnimation ? animationClasses[category] : '' }; }; exports.getSeverityClasses = getSeverityClasses; /** * Get accessible severity label for screen readers * * @param severityLevel - The severity level number * @param description - The severity description * @returns Accessible label with additional context * * @example * const label = getAccessibleSeverityLabel(10, 'Good Service'); * // Returns: 'Good Service - No issues reported' */ const getAccessibleSeverityLabel = (severityLevel, description) => { const category = (0, exports.getSeverityCategory)(severityLevel); const contextMap = { critical: ' - Service disruption affecting travel', severe: ' - Significant delays expected', minor: ' - Minor delays possible', special: ' - Special service information', good: ' - No issues reported' }; return `${description}${contextMap[category]}`; }; exports.getAccessibleSeverityLabel = getAccessibleSeverityLabel; /** * Get line order index for sorting * * @param lineId - The line ID to get order for * @returns Order index (lower = higher priority) * * @example * const order = getLineOrder('central'); // 0 (highest priority) * const order = getLineOrder('unknown'); // LINE_ORDER.length (lowest priority) */ const getLineOrder = (lineId) => { const index = exports.LINE_ORDER.indexOf(lineId); return index === -1 ? exports.LINE_ORDER.length : index; }; exports.getLineOrder = getLineOrder; /** * Check if line statuses indicate normal service * * @param statuses - Array of line status objects * @returns True if all statuses indicate good or special service * * @example * const isNormal = isNormalService(line.lineStatuses); * if (isNormal) { * // Apply normal service styling * } */ const isNormalService = (statuses) => { return statuses.every(status => { const severity = status.statusSeverity; if (severity === undefined) return false; return exports.SEVERITY_MAPPING.good.includes(severity) || exports.SEVERITY_MAPPING.special.includes(severity); }); }; exports.isNormalService = isNormalService; /** * Check if line has night closure * * @param statuses - Array of line status objects * @returns True if any status indicates night closure (severity 20) * * @example * const hasNightClosure = hasNightService(line.lineStatuses); * if (hasNightClosure) { * // Show night closure indicator * } */ const hasNightService = (statuses) => { return statuses.some(status => status.statusSeverity === 20); }; exports.hasNightService = hasNightService; /** * Get ARIA label for line status * * @param lineName - The line name * @param statuses - Array of line status objects * @returns Accessible ARIA label * * @example * const ariaLabel = getLineAriaLabel('Central', line.lineStatuses); * // Returns: 'Central line: Good Service - No issues reported' */ const getLineAriaLabel = (lineName, statuses) => { if (!statuses.length) { return `${lineName} line: No status information available`; } const statusLabels = statuses.map(status => { if (!status.statusSeverity || !status.statusSeverityDescription) { return 'Unknown status'; } return (0, exports.getAccessibleSeverityLabel)(status.statusSeverity, status.statusSeverityDescription); }); return `${lineName} line: ${statusLabels.join(', ')}`; }; exports.getLineAriaLabel = getLineAriaLabel; /** * Get CSS custom properties for line colors * * @param lineId - The line ID * @returns CSS custom properties object * * @example * const cssProps = getLineCssProps('central'); * // Returns: { '--line-color': '#E32017', '--line-color-rgb': '227, 32, 23' } */ const getLineCssProps = (lineId) => { const color = (0, exports.getLineColor)(lineId); const hex = color.hex; // Convert hex to RGB const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return { '--line-color': hex, '--line-color-rgb': `${r}, ${g}, ${b}`, '--line-color-contrast': color.poorDarkContrast ? '#ffffff' : '#000000' }; }; exports.getLineCssProps = getLineCssProps; /** * Sort lines by severity and importance * * @param lines - Array of line objects with status information * @returns Sorted array of lines * * @example * const sortedLines = sortLinesBySeverityAndOrder(lineStatuses); */ const sortLinesBySeverityAndOrder = (lines) => { return lines.sort((a, b) => { const aMinSeverity = Math.min(...(a.lineStatuses?.map(s => s.statusSeverity || 0) || [])); const bMinSeverity = Math.min(...(b.lineStatuses?.map(s => s.statusSeverity || 0) || [])); // If both lines have normal service, sort by predefined order if ((0, exports.isNormalService)(a.lineStatuses || []) && (0, exports.isNormalService)(b.lineStatuses || [])) { return (0, exports.getLineOrder)(a.id || '') - (0, exports.getLineOrder)(b.id || ''); } // If severities are different, sort by severity (lower = more severe) if (aMinSeverity !== bMinSeverity) { return aMinSeverity - bMinSeverity; } // If both lines have the same severity level (but not normal), // still sort by predefined order as a fallback return (0, exports.getLineOrder)(a.id || '') - (0, exports.getLineOrder)(b.id || ''); }); }; exports.sortLinesBySeverityAndOrder = sortLinesBySeverityAndOrder; /** * Get line display name with mode indicator * * @param lineName - The line name * @param modeName - The transport mode * @returns Formatted display name * * @example * const displayName = getLineDisplayName('Central', 'tube'); // 'Central' * const displayName = getLineDisplayName('Liberty', 'overground'); // 'Liberty (Overground)' */ const getLineDisplayName = (lineName, modeName) => { // For tube lines, just use the name if (modeName === 'tube') { return lineName; } // For other modes, add mode indicator const modeDisplayNames = { 'overground': 'Overground', 'elizabeth-line': 'Elizabeth line', 'dlr': 'DLR', 'tram': 'Tram', 'bus': 'Bus', 'river-bus': 'River Bus', 'river-tour': 'River Tour', 'cable-car': 'Cable Car', 'cycle-hire': 'Cycle Hire', 'national-rail': 'National Rail' }; const modeDisplay = modeDisplayNames[modeName] || modeName; return `${lineName} (${modeDisplay})`; }; exports.getLineDisplayName = getLineDisplayName; /** * Get line status summary for quick overview * * @param statuses - Array of line status objects * @returns Summary object with worst severity and count * * @example * const summary = getLineStatusSummary(line.lineStatuses); * // Returns: { worstSeverity: 6, worstDescription: 'Severe Delays', hasIssues: true, issueCount: 1 } */ const getLineStatusSummary = (statuses) => { if (!statuses.length) { return { worstSeverity: 10, worstDescription: 'No Status', hasIssues: false, issueCount: 0 }; } const validStatuses = statuses.filter(s => s.statusSeverity !== undefined); if (!validStatuses.length) { return { worstSeverity: 10, worstDescription: 'Unknown Status', hasIssues: false, issueCount: 0 }; } const worstStatus = validStatuses.reduce((worst, current) => { return (current.statusSeverity || 10) < (worst.statusSeverity || 10) ? current : worst; }); const hasIssues = !(0, exports.isNormalService)(validStatuses); return { worstSeverity: worstStatus.statusSeverity || 10, worstDescription: worstStatus.statusSeverityDescription || 'Unknown', hasIssues, issueCount: hasIssues ? validStatuses.length : 0 }; }; exports.getLineStatusSummary = getLineStatusSummary;