[READ-ONLY] a fast, modern browser for the npm registry
at main 306 lines 8.4 kB view raw
1/** 2 * Package list preferences types 3 * Used for configurable columns, filtering, sorting, and pagination 4 */ 5 6// View modes 7export type ViewMode = 'cards' | 'table' 8 9// Column identifiers for table view 10export type ColumnId = 11 | 'name' 12 | 'version' 13 | 'description' 14 | 'downloads' 15 | 'updated' 16 | 'maintainers' 17 | 'keywords' 18 | 'qualityScore' 19 | 'popularityScore' 20 | 'maintenanceScore' 21 | 'combinedScore' 22 | 'security' 23 24export interface ColumnConfig { 25 id: ColumnId 26 visible: boolean 27 sortable: boolean 28 width?: string 29 /** Whether the column is disabled (not yet available) */ 30 disabled?: boolean 31} 32 33// Default column configuration 34export const DEFAULT_COLUMNS: ColumnConfig[] = [ 35 { id: 'name', visible: true, sortable: true, width: 'minmax(200px, 1fr)' }, 36 { id: 'version', visible: true, sortable: false, width: '100px' }, 37 { 38 id: 'description', 39 visible: true, 40 sortable: false, 41 width: 'minmax(200px, 2fr)', 42 }, 43 { id: 'downloads', visible: true, sortable: true, width: '120px' }, 44 { id: 'updated', visible: true, sortable: true, width: '120px' }, 45 { id: 'maintainers', visible: false, sortable: false, width: '150px' }, 46 { id: 'keywords', visible: false, sortable: false, width: '200px' }, 47 { 48 id: 'qualityScore', 49 visible: false, 50 sortable: true, 51 width: '100px', 52 disabled: true, 53 }, 54 { 55 id: 'popularityScore', 56 visible: false, 57 sortable: true, 58 width: '100px', 59 disabled: true, 60 }, 61 { 62 id: 'maintenanceScore', 63 visible: false, 64 sortable: true, 65 width: '100px', 66 disabled: true, 67 }, 68 { 69 id: 'combinedScore', 70 visible: false, 71 sortable: true, 72 width: '100px', 73 disabled: true, 74 }, 75 { 76 id: 'security', 77 visible: false, 78 sortable: false, 79 width: '80px', 80 disabled: true, 81 }, 82] 83 84// Sort keys (without direction) 85export type SortKey = 86 | 'downloads-week' 87 | 'downloads-day' 88 | 'downloads-month' 89 | 'downloads-year' 90 | 'updated' 91 | 'name' 92 | 'quality' 93 | 'popularity' 94 | 'maintenance' 95 | 'score' 96 | 'relevance' 97 98export type SortDirection = 'asc' | 'desc' 99 100// Combined sort option (key + direction) 101export type SortOption = 102 | 'downloads-week-desc' 103 | 'downloads-week-asc' 104 | 'downloads-day-desc' 105 | 'downloads-day-asc' 106 | 'downloads-month-desc' 107 | 'downloads-month-asc' 108 | 'downloads-year-desc' 109 | 'downloads-year-asc' 110 | 'updated-desc' 111 | 'updated-asc' 112 | 'name-asc' 113 | 'name-desc' 114 | 'quality-desc' 115 | 'quality-asc' 116 | 'popularity-desc' 117 | 'popularity-asc' 118 | 'maintenance-desc' 119 | 'maintenance-asc' 120 | 'score-desc' 121 | 'score-asc' 122 | 'relevance-desc' 123 | 'relevance-asc' 124 125export interface SortKeyConfig { 126 key: SortKey 127 /** Default direction for this sort key */ 128 defaultDirection: SortDirection 129 /** Whether the sort option is disabled (not yet available) */ 130 disabled?: boolean 131 /** Only show this sort option in search context */ 132 searchOnly?: boolean 133} 134 135export const SORT_KEYS: SortKeyConfig[] = [ 136 { key: 'relevance', defaultDirection: 'desc', searchOnly: true }, 137 { key: 'downloads-week', defaultDirection: 'desc' }, 138 { key: 'downloads-day', defaultDirection: 'desc', disabled: true }, 139 { key: 'downloads-month', defaultDirection: 'desc', disabled: true }, 140 { key: 'downloads-year', defaultDirection: 'desc', disabled: true }, 141 { key: 'updated', defaultDirection: 'desc' }, 142 { key: 'name', defaultDirection: 'asc' }, 143 // quality/popularity/maintenance: npm returns 1 for all, Algolia returns synthetic values. 144 // Neither provider produces meaningful values for these. 145 { key: 'quality', defaultDirection: 'desc', disabled: true }, 146 { key: 'popularity', defaultDirection: 'desc', disabled: true }, 147 { key: 'maintenance', defaultDirection: 'desc', disabled: true }, 148 // score.final === searchScore (identical to relevance), redundant sort key 149 { key: 'score', defaultDirection: 'desc', disabled: true }, 150] 151 152/** 153 * Sort keys each search provider can meaningfully sort by. 154 * 155 * Both providers support: relevance (server-side order), updated, name. 156 * 157 * Algolia: has `downloadsLast30Days` for download sorting. 158 * 159 * npm: the search API now includes `downloads.weekly` and `downloads.monthly` 160 * directly in results, so download sorting works here too. 161 * 162 * Neither provider returns useful quality/popularity/maintenance/score values: 163 * - npm returns 1 for all detail scores, and score.final === searchScore (= relevance) 164 * - Algolia returns synthetic values (quality: 0|1, maintenance: 0, score: 0) 165 */ 166export const PROVIDER_SORT_KEYS: Record<'algolia' | 'npm', Set<SortKey>> = { 167 algolia: new Set<SortKey>(['relevance', 'downloads-week', 'updated', 'name']), 168 npm: new Set<SortKey>(['relevance', 'downloads-week', 'updated', 'name']), 169} 170 171/** All valid sort keys for validation */ 172const VALID_SORT_KEYS = new Set<SortKey>([ 173 'relevance', 174 'downloads-week', 175 'downloads-day', 176 'downloads-month', 177 'downloads-year', 178 'updated', 179 'name', 180 'quality', 181 'popularity', 182 'maintenance', 183 'score', 184]) 185 186/** Parse a SortOption into key and direction */ 187export function parseSortOption(option: SortOption): { 188 key: SortKey 189 direction: SortDirection 190} { 191 const match = option.match(/^(.+)-(asc|desc)$/) 192 if (match) { 193 const key = match[1] 194 const direction = match[2] as SortDirection 195 // Validate that the key is a known sort key 196 if (VALID_SORT_KEYS.has(key as SortKey)) { 197 return { key: key as SortKey, direction } 198 } 199 } 200 // Fallback to default sort option 201 return { key: 'downloads-week', direction: 'desc' } 202} 203 204/** Build a SortOption from key and direction */ 205export function buildSortOption(key: SortKey, direction: SortDirection): SortOption { 206 return `${key}-${direction}` as SortOption 207} 208 209/** Get the opposite direction */ 210export function toggleDirection(direction: SortDirection): SortDirection { 211 return direction === 'asc' ? 'desc' : 'asc' 212} 213 214// Download range presets 215export type DownloadRange = 'any' | 'lt100' | '100-1k' | '1k-10k' | '10k-100k' | 'gt100k' 216 217export interface DownloadRangeConfig { 218 value: DownloadRange 219 min?: number 220 max?: number 221} 222 223export const DOWNLOAD_RANGES: DownloadRangeConfig[] = [ 224 { value: 'any' }, 225 { value: 'lt100', max: 100 }, 226 { value: '100-1k', min: 100, max: 1000 }, 227 { value: '1k-10k', min: 1000, max: 10000 }, 228 { value: '10k-100k', min: 10000, max: 100000 }, 229 { value: 'gt100k', min: 100000 }, 230] 231 232// Updated within presets 233export type UpdatedWithin = 'any' | 'week' | 'month' | 'quarter' | 'year' 234 235export interface UpdatedWithinConfig { 236 value: UpdatedWithin 237 days?: number 238} 239 240export const UPDATED_WITHIN_OPTIONS: UpdatedWithinConfig[] = [ 241 { value: 'any' }, 242 { value: 'week', days: 7 }, 243 { value: 'month', days: 30 }, 244 { value: 'quarter', days: 90 }, 245 { value: 'year', days: 365 }, 246] 247 248// Security filter options 249export type SecurityFilter = 'all' | 'secure' | 'warnings' 250 251/** Security filter values - labels are in i18n under filters.security_options */ 252export const SECURITY_FILTER_VALUES: SecurityFilter[] = ['all', 'secure', 'warnings'] 253 254// Search scope options 255export type SearchScope = 'name' | 'description' | 'keywords' | 'all' 256 257/** Search scope values - labels are in i18n under filters.scope_* */ 258export const SEARCH_SCOPE_VALUES: SearchScope[] = ['name', 'description', 'keywords', 'all'] 259 260// Structured filters state 261export interface StructuredFilters { 262 text: string 263 searchScope: SearchScope 264 downloadRange: DownloadRange 265 keywords: string[] 266 security: SecurityFilter 267 updatedWithin: UpdatedWithin 268} 269 270export const DEFAULT_FILTERS: StructuredFilters = { 271 text: '', 272 searchScope: 'name', 273 downloadRange: 'any', 274 keywords: [], 275 security: 'all', 276 updatedWithin: 'any', 277} 278 279// Pagination modes 280export type PaginationMode = 'infinite' | 'paginated' 281 282export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100, 'all'] as const 283export type PageSize = (typeof PAGE_SIZE_OPTIONS)[number] 284 285// Complete preferences state 286export interface PackageListPreferences { 287 viewMode: ViewMode 288 columns: ColumnConfig[] 289 paginationMode: PaginationMode 290 pageSize: PageSize 291} 292 293export const DEFAULT_PREFERENCES: PackageListPreferences = { 294 viewMode: 'cards', 295 columns: DEFAULT_COLUMNS, 296 paginationMode: 'infinite', 297 pageSize: 25, 298} 299 300// Active filter chip representation 301export interface FilterChip { 302 id: string 303 type: keyof StructuredFilters 304 label: string 305 value: string | string[] 306}