Skip to content

Commit a1c5fd2

Browse files
committed
Merge remote-tracking branch 'origin/main' into i18n/az-locale-update
2 parents 3bbeb52 + 0d82d42 commit a1c5fd2

53 files changed

Lines changed: 2018 additions & 731 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/assets/main.css

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,16 @@ dd {
275275
}
276276

277277
/* Shiki theme colors */
278-
html.light .shiki,
279-
html.light .shiki span {
278+
html.light .shiki {
280279
color: var(--shiki-light) !important;
281280
background-color: var(--shiki-light-bg) !important;
282-
font-style: var(--shiki-light-font-style) !important;
283-
font-weight: var(--shiki-light-font-weight) !important;
284-
text-decoration: var(--shiki-light-text-decoration) !important;
281+
282+
& span {
283+
color: var(--shiki-light) !important;
284+
font-style: var(--shiki-light-font-style) !important;
285+
font-weight: var(--shiki-light-font-weight) !important;
286+
text-decoration: var(--shiki-light-text-decoration) !important;
287+
}
285288
}
286289

287290
/* Inline code in package descriptions */
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
<script setup lang="ts">
2+
import { ref, computed } from 'vue'
3+
import { VueUiHorizontalBar } from 'vue-data-ui/vue-ui-horizontal-bar'
4+
import type {
5+
VueUiHorizontalBarConfig,
6+
VueUiHorizontalBarDatapoint,
7+
VueUiHorizontalBarDatasetItem,
8+
} from 'vue-data-ui'
9+
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
10+
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
11+
import {
12+
loadFile,
13+
insertLineBreaks,
14+
sanitise,
15+
applyEllipsis,
16+
copyAltTextForCompareFacetBarChart,
17+
} from '~/utils/charts'
18+
19+
import('vue-data-ui/style.css')
20+
21+
const props = defineProps<{
22+
values: (FacetValue | null | undefined)[]
23+
packages: string[]
24+
label: string
25+
description: string
26+
facetLoading?: boolean
27+
}>()
28+
29+
const colorMode = useColorMode()
30+
const resolvedMode = shallowRef<'light' | 'dark'>('light')
31+
const rootEl = shallowRef<HTMLElement | null>(null)
32+
const { width } = useElementSize(rootEl)
33+
const { copy, copied } = useClipboard()
34+
35+
const mobileBreakpointWidth = 640
36+
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)
37+
38+
const chartKey = ref(0)
39+
40+
const { colors } = useCssVariables(
41+
[
42+
'--bg',
43+
'--fg',
44+
'--bg-subtle',
45+
'--bg-elevated',
46+
'--fg-subtle',
47+
'--fg-muted',
48+
'--border',
49+
'--border-subtle',
50+
],
51+
{
52+
element: rootEl,
53+
watchHtmlAttributes: true,
54+
watchResize: false,
55+
},
56+
)
57+
58+
const watermarkColors = computed(() => ({
59+
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,
60+
bg: colors.value.bg ?? OKLCH_NEUTRAL_FALLBACK,
61+
fgSubtle: colors.value.fgSubtle ?? OKLCH_NEUTRAL_FALLBACK,
62+
}))
63+
64+
onMounted(async () => {
65+
rootEl.value = document.documentElement
66+
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
67+
})
68+
69+
watch(
70+
() => colorMode.value,
71+
value => {
72+
resolvedMode.value = value === 'dark' ? 'dark' : 'light'
73+
},
74+
{ flush: 'sync' },
75+
)
76+
77+
watch(
78+
() => props.packages,
79+
(newP, oldP) => {
80+
if (newP.length !== oldP.length) return
81+
chartKey.value += 1
82+
},
83+
)
84+
85+
const isDarkMode = computed(() => resolvedMode.value === 'dark')
86+
87+
const dataset = computed<VueUiHorizontalBarDatasetItem[]>(() => {
88+
if (props.facetLoading) return []
89+
return props.packages.map((name, index) => {
90+
const rawValue = props.values[index]?.raw
91+
return {
92+
name: insertLineBreaks(applyEllipsis(name)),
93+
value: typeof rawValue === 'number' ? rawValue : 0,
94+
color: isListedFramework(name) ? getFrameworkColor(name) : undefined,
95+
formattedValue: props.values[index]?.display,
96+
}
97+
})
98+
})
99+
100+
const skeletonDataset = computed(() =>
101+
props.packages.map((_pkg, i) => ({
102+
name: '_',
103+
value: i + 1,
104+
color: colors.value.border,
105+
})),
106+
)
107+
108+
function buildExportFilename(extension: string): string {
109+
const sanitizedPackages = props.packages.map(p => sanitise(p).slice(0, 10)).join('_')
110+
const comparisonLabel = sanitise($t('compare.packages.section_comparison'))
111+
const facetLabel = sanitise(props.label)
112+
return `${facetLabel}_${comparisonLabel}_${sanitizedPackages}.${extension}`
113+
}
114+
115+
const config = computed<VueUiHorizontalBarConfig>(() => {
116+
return {
117+
theme: isDarkMode.value ? 'dark' : '',
118+
userOptions: {
119+
buttons: {
120+
tooltip: false,
121+
pdf: false,
122+
fullscreen: false,
123+
sort: false,
124+
annotator: false,
125+
table: false,
126+
csv: false,
127+
altCopy: true,
128+
},
129+
buttonTitle: {
130+
img: $t('package.trends.download_file', { fileType: 'PNG' }),
131+
svg: $t('package.trends.download_file', { fileType: 'SVG' }),
132+
altCopy: $t('package.trends.copy_alt.button_label'),
133+
},
134+
callbacks: {
135+
img: args => {
136+
const imageUri = args?.imageUri
137+
if (!imageUri) return
138+
loadFile(imageUri, buildExportFilename('png'))
139+
},
140+
svg: args => {
141+
const blob = args?.blob
142+
if (!blob) return
143+
const url = URL.createObjectURL(blob)
144+
loadFile(url, buildExportFilename('svg'))
145+
URL.revokeObjectURL(url)
146+
},
147+
altCopy: ({ dataset: dst, config: cfg }) => {
148+
copyAltTextForCompareFacetBarChart({
149+
dataset: dst,
150+
config: {
151+
...cfg,
152+
facet: props.label,
153+
description: props.description,
154+
copy,
155+
$t,
156+
},
157+
})
158+
},
159+
},
160+
},
161+
skeletonDataset: skeletonDataset.value,
162+
skeletonConfig: {
163+
style: {
164+
chart: {
165+
backgroundColor: colors.value.bg,
166+
},
167+
},
168+
},
169+
style: {
170+
chart: {
171+
backgroundColor: colors.value.bg,
172+
height: 60 * props.packages.length,
173+
layout: {
174+
bars: {
175+
rowColor: isDarkMode.value ? colors.value.borderSubtle : colors.value.bgSubtle,
176+
rowRadius: 4,
177+
borderRadius: 4,
178+
dataLabels: {
179+
fontSize: isMobile.value ? 12 : 18,
180+
percentage: { show: false },
181+
offsetX: 12,
182+
bold: false,
183+
color: colors.value.fg,
184+
value: {
185+
formatter: ({ config }) => {
186+
return config?.datapoint?.formattedValue ?? '0'
187+
},
188+
},
189+
},
190+
nameLabels: {
191+
fontSize: isMobile.value ? 12 : 18,
192+
color: colors.value.fgSubtle,
193+
},
194+
underlayerColor: colors.value.bg,
195+
},
196+
highlighter: {
197+
opacity: isMobile.value ? 0 : 5,
198+
},
199+
},
200+
legend: {
201+
show: false,
202+
},
203+
title: {
204+
fontSize: 16,
205+
bold: false,
206+
text: props.label,
207+
color: colors.value.fg,
208+
subtitle: {
209+
text: props.description,
210+
fontSize: 12,
211+
color: colors.value.fgSubtle,
212+
},
213+
},
214+
tooltip: {
215+
show: !isMobile.value,
216+
borderColor: 'transparent',
217+
backdropFilter: false,
218+
backgroundColor: 'transparent',
219+
customFormat: ({ datapoint }) => {
220+
const name = datapoint?.name?.replace(/\n/g, '<br>')
221+
return `
222+
<div class="font-mono p-3 border border-border rounded-md bg-[var(--bg)]/10 backdrop-blur-md">
223+
<div class="grid grid-cols-[12px_minmax(0,1fr)_max-content] items-center gap-x-3">
224+
<div class="w-3 h-3">
225+
<svg viewBox="0 0 2 2" class="w-full h-full">
226+
<rect x="0" y="0" width="2" height="2" rx="0.3" fill="${datapoint?.color}" />
227+
</svg>
228+
</div>
229+
<span class="text-3xs uppercase tracking-wide text-[var(--fg)]/70 truncate">
230+
${name}
231+
</span>
232+
<span class="text-base text-[var(--fg)] font-mono tabular-nums text-end">
233+
${(datapoint as VueUiHorizontalBarDatapoint).formattedValue ?? 0}
234+
</span>
235+
</div>
236+
</div>
237+
`
238+
},
239+
},
240+
},
241+
},
242+
}
243+
})
244+
</script>
245+
246+
<template>
247+
<div class="font-mono facet-bar">
248+
<ClientOnly v-if="dataset.length">
249+
<VueUiHorizontalBar :key="chartKey" :dataset :config class="[direction:ltr]">
250+
<template #svg="{ svg }">
251+
<!-- Inject npmx logo & tagline during SVG and PNG print -->
252+
<g
253+
v-if="svg.isPrintingSvg || svg.isPrintingImg"
254+
v-html="
255+
drawSmallNpmxLogoAndTaglineWatermark({
256+
svg,
257+
colors: watermarkColors,
258+
translateFn: $t,
259+
})
260+
"
261+
/>
262+
</template>
263+
264+
<template #menuIcon="{ isOpen }">
265+
<span v-if="isOpen" class="i-lucide:x w-6 h-6" aria-hidden="true" />
266+
<span v-else class="i-lucide:ellipsis-vertical w-6 h-6" aria-hidden="true" />
267+
</template>
268+
<template #optionCsv>
269+
<span class="text-fg-subtle font-mono pointer-events-none">CSV</span>
270+
</template>
271+
<template #optionImg>
272+
<span class="text-fg-subtle font-mono pointer-events-none">PNG</span>
273+
</template>
274+
<template #optionSvg>
275+
<span class="text-fg-subtle font-mono pointer-events-none">SVG</span>
276+
</template>
277+
<template #optionAltCopy>
278+
<span
279+
class="w-6 h-6"
280+
:class="
281+
copied ? 'i-lucide:check text-accent' : 'i-lucide:person-standing text-fg-subtle'
282+
"
283+
style="pointer-events: none"
284+
aria-hidden="true"
285+
/>
286+
</template>
287+
</VueUiHorizontalBar>
288+
289+
<template #fallback>
290+
<div class="flex flex-col gap-2 justify-center items-center mb-2">
291+
<SkeletonInline class="h-4 w-16" />
292+
<SkeletonInline class="h-4 w-28" />
293+
</div>
294+
<div class="flex flex-col gap-1">
295+
<SkeletonInline class="h-7 w-full" v-for="pkg in packages" :key="pkg" />
296+
</div>
297+
</template>
298+
</ClientOnly>
299+
300+
<template v-else>
301+
<div class="flex flex-col gap-2 justify-center items-center mb-2">
302+
<SkeletonInline class="h-4 w-16" />
303+
<SkeletonInline class="h-4 w-28" />
304+
</div>
305+
<div class="flex flex-col gap-1">
306+
<SkeletonInline class="h-7 w-full" v-for="pkg in packages" :key="pkg" />
307+
</div>
308+
</template>
309+
</div>
310+
</template>
311+
312+
<style>
313+
.facet-bar .atom-subtitle {
314+
width: 80% !important;
315+
margin: 0 auto;
316+
height: 2rem;
317+
}
318+
</style>

app/components/Header/SearchBox.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ defineExpose({ focus })
4444

4545
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
4646
<div class="search-box relative flex items-center">
47-
<span
48-
class="absolute inset-is-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none [.group:hover:not(:focus-within)_&]:text-fg/80 group-focus-within:text-accent z-1"
47+
<kbd
48+
class="absolute inset-is-3 text-fg-subtle font-mono text-sm pointer-events-none transition-colors duration-200 motion-reduce:transition-none [.group:hover:not(:focus-within)_&]:text-fg/80 group-focus-within:text-accent z-1 rounded"
49+
aria-hidden="true"
4950
>
5051
/
51-
</span>
52+
</kbd>
5253

5354
<InputBase
5455
id="header-search"
@@ -62,6 +63,7 @@ defineExpose({ focus })
6263
@focus="isSearchFocused = true"
6364
@blur="isSearchFocused = false"
6465
size="small"
66+
ariaKeyshortcuts="/"
6567
/>
6668
<button
6769
v-if="hasSearchQuery"

app/components/Input/Base.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ const props = withDefaults(
1414
* @default true
1515
*/
1616
noCorrect?: boolean
17+
/** Keyboard shortcut hint */
18+
ariaKeyshortcuts?: string
1719
}>(),
1820
{
1921
size: 'medium',
@@ -28,6 +30,8 @@ const emit = defineEmits<{
2830
2931
const el = useTemplateRef('el')
3032
33+
const keyboardShortcutsEnabled = useKeyboardShortcuts()
34+
3135
defineExpose({
3236
focus: () => el.value?.focus(),
3337
blur: () => el.value?.blur(),
@@ -51,5 +55,6 @@ defineExpose({
5155
/** Catching Vue render-bug of invalid `disabled=false` attribute in the final HTML */
5256
disabled ? true : undefined
5357
"
58+
:aria-keyshortcuts="keyboardShortcutsEnabled ? ariaKeyshortcuts : undefined"
5459
/>
5560
</template>

0 commit comments

Comments
 (0)