forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2import type { FacetValue } from '#shared/types'
3
4const props = defineProps<{
5 /** Facet label */
6 label: string
7 /** Description/tooltip for the facet */
8 description?: string
9 /** Values for each column */
10 values: (FacetValue | null | undefined)[]
11 /** Whether this facet is loading (e.g., install size) */
12 facetLoading?: boolean
13 /** Whether each column is loading (array matching values) */
14 columnLoading?: boolean[]
15 /** Whether to show the proportional bar (defaults to true for numeric values) */
16 bar?: boolean
17}>()
18
19// Check if all values are numeric (for bar visualization)
20const isNumeric = computed(() => {
21 return props.values.every(v => v === null || v === undefined || typeof v.raw === 'number')
22})
23
24// Show bar if explicitly enabled, or if not specified and values are numeric
25const showBar = computed(() => {
26 return props.bar ?? isNumeric.value
27})
28
29// Get max value for bar width calculation
30const maxValue = computed(() => {
31 if (!isNumeric.value) return 0
32 return Math.max(...props.values.map(v => (typeof v?.raw === 'number' ? v.raw : 0)))
33})
34
35// Calculate bar width percentage for a value
36function getBarWidth(value: FacetValue | null | undefined): number {
37 if (!isNumeric.value || !maxValue.value || !value || typeof value.raw !== 'number') return 0
38 return (value.raw / maxValue.value) * 100
39}
40
41function getStatusClass(status?: FacetValue['status'], hasBar = false): string {
42 // When there's a bar, only apply text color, not background
43 if (hasBar) {
44 switch (status) {
45 case 'muted':
46 return 'text-fg-subtle'
47 default:
48 return 'text-fg'
49 }
50 }
51
52 // Original behavior when no bar
53 switch (status) {
54 case 'good':
55 return 'bg-emerald-400/20 px-3 py-0.5 rounded-xl'
56 case 'info':
57 return 'bg-blue-400/20 px-3 py-0.5 rounded-xl'
58 case 'warning':
59 return 'bg-amber-400/20 px-3 py-0.5 rounded-xl'
60 case 'bad':
61 return 'bg-red-400/20 px-3 py-0.5 rounded-xl'
62 case 'muted':
63 return 'text-fg-subtle'
64 default:
65 return 'text-fg'
66 }
67}
68
69function getStatusBarClass(status?: FacetValue['status']): string {
70 switch (status) {
71 case 'good':
72 return 'bg-emerald-500/20'
73 case 'info':
74 return 'bg-blue-500/20'
75 case 'warning':
76 return 'bg-amber-500/20'
77 case 'bad':
78 return 'bg-red-500/20'
79 default:
80 return 'bg-fg/5'
81 }
82}
83
84// Check if a specific cell is loading
85function isCellLoading(index: number): boolean {
86 return props.facetLoading || (props.columnLoading?.[index] ?? false)
87}
88</script>
89
90<template>
91 <div class="contents">
92 <!-- Label cell -->
93 <div class="comparison-label flex items-center gap-1.5 px-4 py-3 border-b border-border">
94 <span class="text-xs text-fg-muted uppercase tracking-wider">{{ label }}</span>
95 <TooltipApp v-if="description" :text="description" position="top">
96 <span class="i-lucide:info w-3 h-3 text-fg-subtle cursor-help" aria-hidden="true" />
97 </TooltipApp>
98 </div>
99
100 <!-- Value cells -->
101 <div
102 v-for="(value, index) in values"
103 :key="index"
104 class="comparison-cell relative flex items-center justify-center px-4 py-3 border-b border-border"
105 >
106 <!-- Background bar for numeric values -->
107 <div
108 v-if="showBar && value && getBarWidth(value) > 0"
109 class="absolute inset-y-1 inset-is-1 rounded-sm transition-all duration-300"
110 :class="getStatusBarClass(value.status)"
111 :style="{ width: `calc(${getBarWidth(value)}% - 8px)` }"
112 aria-hidden="true"
113 />
114
115 <!-- Loading state -->
116 <template v-if="isCellLoading(index)">
117 <span class="i-svg-spinners:ring-resize w-4 h-4 text-fg-subtle" aria-hidden="true" />
118 </template>
119
120 <!-- No data -->
121 <template v-else-if="!value">
122 <span class="text-fg-subtle text-sm">-</span>
123 </template>
124
125 <!-- Value display -->
126 <template v-else>
127 <TooltipApp v-if="value.tooltip" :text="value.tooltip" position="top">
128 <span
129 class="relative font-mono text-sm text-center tabular-nums cursor-help"
130 :class="getStatusClass(value.status, showBar && getBarWidth(value) > 0)"
131 :data-status="value.status"
132 >
133 <!-- Date values use DateTime component for i18n and user settings -->
134 <DateTime v-if="value.type === 'date'" :datetime="value.display" date-style="medium" />
135 <template v-else>
136 <span dir="auto">{{ value.display }}</span>
137 </template>
138 </span>
139 </TooltipApp>
140 <span
141 v-else
142 class="relative font-mono text-sm text-center tabular-nums"
143 :class="getStatusClass(value.status, showBar && getBarWidth(value) > 0)"
144 :data-status="value.status"
145 >
146 <!-- Date values use DateTime component for i18n and user settings -->
147 <DateTime v-if="value.type === 'date'" :datetime="value.display" date-style="medium" />
148 <template v-else>
149 <span dir="auto">{{ value.display }}</span>
150 </template>
151 </span>
152 </template>
153 </div>
154 </div>
155</template>