-
-
Notifications
You must be signed in to change notification settings - Fork 433
feat(ui): community version distribution #1245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 5 commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
79ef365
feat(frontend): community version distribution
vinnymac abf5521
Merge remote-tracking branch 'upstream/main' into vt/nullvoxpopuli
vinnymac 6b8b10c
fix(ui): tooltip z index and positioning
vinnymac a841c6e
fix(ui): update toggle to make it reusable
vinnymac 7afacda
fix(ui): remove unnecessary css selectors
vinnymac 5bc8d85
chore: fix class types
vinnymac 25baf03
chore: remove dead code from original color implementation
vinnymac 031a13f
chore: fix usage of any type
vinnymac 6f3e7cb
fix: handle out of range
vinnymac fb43c42
fix: i18n title keys
vinnymac d17e00a
fix: add package name to y axis label for better screenshots
vinnymac 9eb9dfa
fix: a11y spec types
vinnymac 95c7554
feat(ui): update the ui to better match the existing modal and improv…
vinnymac 418ec4f
fix: a11y focus outlines
vinnymac 482bc84
fix: prevent keydown while loading
vinnymac b6e063a
feat: add show low usage versions
vinnymac bff1505
feat: flip the wording for old versions so we default to hiding them
vinnymac 4919d6f
fix: remove unnecessary tick and fix aspect ratio
vinnymac d264b7d
fix: add timeout cleanup
vinnymac e65bf62
fix: caching best practices
vinnymac 205c634
fix: last week version nitpicks
vinnymac 75c778f
fix: revert ai recommendation
vinnymac bcf6f43
fix: move validations into try/catch
vinnymac e9ea5c1
chore: merge conflicts resolved
vinnymac 277b6a0
chore: i18n schema with version distribution strings
vinnymac 20e8826
fix: isr cache key got me dizzy
vinnymac f0a386d
fix: chart enhancements from feedback
vinnymac b02f876
Merge branch 'main' into vt/nullvoxpopuli
vinnymac 43394a0
fix: tooltips in version distributions modal
vinnymac 6ff63b8
Merge branch 'main' into vt/nullvoxpopuli
vinnymac 99073d9
feat: reverse order for toggles
vinnymac b889847
feat: subtle gradient for area charts
vinnymac 7a49dad
fix: add missing modal title
vinnymac f6b93d5
fix: position of reset button
vinnymac 1230cd1
fix: switch back to old versions by default
vinnymac f9edd5c
Merge remote-tracking branch 'upstream/main' into vt/nullvoxpopuli
vinnymac 63ab22c
fix: move tooltip and labels
vinnymac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,326 @@ | ||
| <script setup lang="ts"> | ||
| import { VueUiXy } from 'vue-data-ui/vue-ui-xy' | ||
| import type { VueUiXyDatasetItem } from 'vue-data-ui' | ||
| import { useElementSize } from '@vueuse/core' | ||
| import { useCssVariables } from '~/composables/useColors' | ||
| import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors' | ||
|
|
||
| const props = defineProps<{ | ||
| packageName: string | ||
| inModal?: boolean | ||
| }>() | ||
|
|
||
| const { accentColors, selectedAccentColor } = useAccentColor() | ||
| const colorMode = useColorMode() | ||
| const resolvedMode = shallowRef<'light' | 'dark'>('light') | ||
| const rootEl = shallowRef<HTMLElement | null>(null) | ||
|
|
||
| onMounted(async () => { | ||
| rootEl.value = document.documentElement | ||
| resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light' | ||
| }) | ||
|
|
||
| const { colors } = useCssVariables( | ||
| ['--bg', '--fg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'], | ||
| { | ||
| element: rootEl, | ||
| watchHtmlAttributes: true, | ||
| watchResize: false, | ||
| }, | ||
| ) | ||
|
|
||
| watch( | ||
| () => colorMode.value, | ||
| value => { | ||
| resolvedMode.value = value === 'dark' ? 'dark' : 'light' | ||
| }, | ||
| { flush: 'sync' }, | ||
| ) | ||
|
|
||
| const isDarkMode = computed(() => resolvedMode.value === 'dark') | ||
|
|
||
| const accentColorValueById = computed<Record<string, string>>(() => { | ||
| const map: Record<string, string> = {} | ||
| for (const item of accentColors.value) { | ||
| map[item.id] = item.value | ||
| } | ||
| return map | ||
| }) | ||
|
|
||
| const accent = computed(() => { | ||
| const id = selectedAccentColor.value | ||
| return id | ||
| ? (accentColorValueById.value[id] ?? colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK) | ||
| : (colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK) | ||
| }) | ||
|
|
||
| const { width } = useElementSize(rootEl) | ||
| const mobileBreakpointWidth = 640 | ||
| const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth) | ||
|
|
||
| const { groupingMode, hideSmallVersions, pending, error, chartDataset, hasData } = | ||
| useVersionDistribution(() => props.packageName) | ||
|
|
||
| const compactNumberFormatter = useCompactNumberFormatter() | ||
|
|
||
| const chartConfig = computed(() => { | ||
| return { | ||
| theme: isDarkMode.value ? 'dark' : 'default', | ||
| chart: { | ||
| height: isMobile.value ? 500 : 400, | ||
| backgroundColor: colors.value.bg, | ||
| padding: { | ||
| top: 24, | ||
| right: 24, | ||
| bottom: xAxisLabels.value.length > 10 ? 100 : 72, // More space for rotated labels | ||
| left: isMobile.value ? 60 : 80, | ||
| }, | ||
| userOptions: { | ||
| buttons: { pdf: false, labels: false, fullscreen: false, table: false, tooltip: false }, | ||
| }, | ||
| grid: { | ||
| stroke: colors.value.border, | ||
| labels: { | ||
| fontSize: isMobile.value ? 24 : 16, | ||
| color: pending.value ? colors.value.border : colors.value.fgSubtle, | ||
| axis: { | ||
| yLabel: 'Downloads', | ||
| xLabel: '', | ||
| yLabelOffsetX: 12, | ||
| fontSize: isMobile.value ? 32 : 24, | ||
| }, | ||
| yAxis: { | ||
| formatter: ({ value }: { value: number }) => { | ||
| return compactNumberFormatter.value.format(Number.isFinite(value) ? value : 0) | ||
| }, | ||
| useNiceScale: true, | ||
| }, | ||
| xAxisLabels: { | ||
| show: true, | ||
| values: xAxisLabels.value, | ||
| fontSize: isMobile.value ? 14 : 12, | ||
|
vinnymac marked this conversation as resolved.
Outdated
|
||
| color: colors.value.fgSubtle, | ||
| rotation: xAxisLabels.value.length > 10 ? 45 : 0, | ||
| }, | ||
| }, | ||
| }, | ||
| timeTag: { | ||
|
vinnymac marked this conversation as resolved.
Outdated
|
||
| show: false, | ||
| }, | ||
| highlighter: { useLine: false }, | ||
| legend: { show: false }, | ||
| bar: { | ||
| periodGap: 16, | ||
| innerGap: 8, | ||
| borderRadius: 4, | ||
| }, | ||
| tooltip: { | ||
| teleportTo: props.inModal ? '#chart-modal' : undefined, | ||
| borderColor: 'transparent', | ||
| backdropFilter: false, | ||
| backgroundColor: 'transparent', | ||
| customFormat: (params: any) => { | ||
| const { datapoint, absoluteIndex, bars } = params | ||
| if (!datapoint) return '' | ||
|
|
||
| // Use absoluteIndex to get the correct version from chartDataset | ||
| const index = Number(absoluteIndex ?? 0) | ||
| const chartItem = chartDataset.value[index] | ||
|
|
||
| if (!chartItem) return '' | ||
|
|
||
| const barValue = bars?.[0]?.values?.[index] | ||
| const raw = Number(barValue ?? chartItem.downloads ?? 0) | ||
| const v = compactNumberFormatter.value.format(Number.isFinite(raw) ? raw : 0) | ||
|
vinnymac marked this conversation as resolved.
Outdated
|
||
|
|
||
| return `<div class="font-mono text-xs p-3 border border-border rounded-md bg-[var(--bg)]/10 backdrop-blur-md"> | ||
| <div class="flex flex-col gap-2"> | ||
| <div class="flex items-center justify-between gap-4"> | ||
| <span class="text-3xs uppercase tracking-wide text-[var(--fg)]/70"> | ||
| ${chartItem.name} | ||
| </span> | ||
| <span class="text-base text-[var(--fg)] font-mono tabular-nums"> | ||
| ${v} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| </div>` | ||
| }, | ||
| }, | ||
|
vinnymac marked this conversation as resolved.
Outdated
|
||
| zoom: { | ||
| maxWidth: isMobile.value ? 350 : 500, | ||
| highlightColor: colors.value.bgElevated, | ||
| minimap: { | ||
| show: true, | ||
| lineColor: '#FAFAFA', | ||
| selectedColor: accent.value, | ||
| selectedColorOpacity: 0.06, | ||
| frameColor: colors.value.border, | ||
| }, | ||
| preview: { | ||
| fill: transparentizeOklch(accent.value, isDarkMode.value ? 0.95 : 0.92), | ||
| stroke: transparentizeOklch(accent.value, 0.5), | ||
| strokeWidth: 1, | ||
| strokeDasharray: 3, | ||
| }, | ||
| }, | ||
| }, | ||
| userOptions: { | ||
|
vinnymac marked this conversation as resolved.
Outdated
|
||
| show: false, | ||
| }, | ||
| table: { | ||
| show: false, | ||
| }, | ||
| } | ||
| }) | ||
|
|
||
| // VueUiXy expects one series with multiple values for bar charts | ||
| const xyDataset = computed<VueUiXyDatasetItem[]>(() => { | ||
| if (!chartDataset.value.length) return [] | ||
|
|
||
| return [ | ||
| { | ||
| name: 'Downloads', | ||
| series: chartDataset.value.map(item => item.downloads), | ||
| type: 'bar' as const, | ||
| color: accent.value, | ||
| }, | ||
| ] | ||
| }) | ||
|
|
||
| const xAxisLabels = computed(() => { | ||
| return chartDataset.value.map(item => item.name) | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <div | ||
| class="w-full relative" | ||
| :class="isMobile ? 'min-h-[600px]' : 'min-h-[500px]'" | ||
| id="version-distribution" | ||
| :aria-busy="pending ? 'true' : 'false'" | ||
| > | ||
| <div class="w-full mb-4 flex flex-col gap-3"> | ||
| <div class="flex flex-col sm:flex-row gap-3 sm:gap-2 sm:items-end"> | ||
| <div class="flex flex-col gap-1 sm:shrink-0"> | ||
| <label class="text-3xs font-mono text-fg-subtle tracking-wide uppercase"> | ||
| {{ $t('package.versions.distribution_title') }} | ||
| </label> | ||
| <div | ||
| class="inline-flex items-center bg-bg-subtle border border-border rounded-md overflow-hidden w-fit" | ||
| role="group" | ||
| :aria-label="$t('package.versions.distribution_title')" | ||
| > | ||
| <button | ||
| type="button" | ||
| :class="[ | ||
| 'px-4 py-1.75 font-mono text-sm transition-colors', | ||
| groupingMode === 'major' | ||
| ? 'bg-accent text-bg font-medium' | ||
| : 'text-fg-subtle hover:text-fg hover:bg-bg-subtle/50', | ||
| ]" | ||
| :aria-pressed="groupingMode === 'major'" | ||
| :disabled="pending" | ||
| @click="groupingMode = 'major'" | ||
| > | ||
| {{ $t('package.versions.grouping_major') }} | ||
| </button> | ||
| <button | ||
| type="button" | ||
| :class="[ | ||
| 'px-4 py-1.75 font-mono text-sm transition-colors border-is border-border', | ||
| groupingMode === 'minor' | ||
| ? 'bg-accent text-bg font-medium' | ||
| : 'text-fg-subtle hover:text-fg hover:bg-bg-subtle/50', | ||
| ]" | ||
| :aria-pressed="groupingMode === 'minor'" | ||
| :disabled="pending" | ||
| @click="groupingMode = 'minor'" | ||
| > | ||
| {{ $t('package.versions.grouping_minor') }} | ||
| </button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <SettingsToggle | ||
|
Check failure on line 246 in app/components/Package/VersionDistribution.vue
|
||
| v-model="hideSmallVersions" | ||
| :label="$t('package.versions.hide_old_versions')" | ||
| :tooltip="$t('package.versions.hide_old_versions_tooltip')" | ||
| tooltip-position="right" | ||
| :tooltip-teleport-to="inModal ? '#chart-modal' : undefined" | ||
| justify="start" | ||
| :class="{ 'opacity-50 pointer-events-none': pending }" | ||
| /> | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| </div> | ||
|
|
||
| <h2 id="version-distribution-title" class="sr-only"> | ||
| {{ $t('package.versions.distribution_title') }} | ||
| </h2> | ||
|
|
||
| <div | ||
| role="region" | ||
| aria-labelledby="version-distribution-title" | ||
| class="relative flex items-center justify-center" | ||
| :class="isMobile ? 'min-h-[500px]' : 'min-h-[400px]'" | ||
| > | ||
| <div | ||
| v-if="pending" | ||
| role="status" | ||
| aria-live="polite" | ||
| class="text-xs text-fg-subtle font-mono bg-bg/70 backdrop-blur px-3 py-2 rounded-md border border-border" | ||
| > | ||
| {{ $t('common.loading') }} | ||
| </div> | ||
|
|
||
| <div | ||
| v-else-if="error" | ||
| class="text-sm text-fg-subtle font-mono text-center flex flex-col items-center gap-2" | ||
| role="alert" | ||
| > | ||
| <span class="i-carbon:warning-hex w-8 h-8 text-red-400" /> | ||
| <p>{{ error.message }}</p> | ||
| <p class="text-xs">Package: {{ packageName }}</p> | ||
| </div> | ||
|
|
||
| <div | ||
| v-else-if="!hasData" | ||
| class="text-sm text-fg-subtle font-mono text-center flex flex-col items-center gap-2" | ||
| > | ||
| <span class="i-carbon:data-vis-4 w-8 h-8" /> | ||
| <p>{{ $t('package.trends.no_data') }}</p> | ||
| </div> | ||
|
|
||
| <ClientOnly v-else-if="xyDataset.length > 0"> | ||
| <div | ||
| class="chart-container w-full h-[400px] sm:h-[400px]" | ||
| :class="{ 'h-[500px]': isMobile }" | ||
| > | ||
| <VueUiXy :dataset="xyDataset" :config="chartConfig" class="[direction:ltr]" /> | ||
|
vinnymac marked this conversation as resolved.
Outdated
|
||
| </div> | ||
|
vinnymac marked this conversation as resolved.
|
||
| </ClientOnly> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <style scoped> | ||
| /* Disable all transitions on SVG elements to prevent repositioning animation */ | ||
| :deep(.vue-ui-xy) svg rect { | ||
| transition: none !important; | ||
| } | ||
|
vinnymac marked this conversation as resolved.
|
||
|
|
||
| @keyframes fadeInUp { | ||
| from { | ||
| opacity: 0; | ||
| transform: translateY(8px); | ||
| } | ||
| to { | ||
| opacity: 1; | ||
| transform: translateY(0); | ||
| } | ||
| } | ||
|
|
||
| .chart-container { | ||
| animation: fadeInUp 350ms cubic-bezier(0.4, 0, 0.2, 1); | ||
| } | ||
| </style> | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.