forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
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}