1+ <script setup lang="ts">
2+ import { VueUiSparkline } from ' vue-data-ui/vue-ui-sparkline'
3+ import { useCssVariables } from ' ~/composables/useColors'
4+ import { getPalette , type VueUiXyDatasetItem } from " vue-data-ui" ;
5+ import type { VueUiSparklineConfig , VueUiSparklineDatasetItem } from ' vue-data-ui'
6+
7+ import (' vue-data-ui/style.css' )
8+
9+ const props = defineProps <{
10+ dataset? : Array <VueUiXyDatasetItem & {
11+ color? : string
12+ series: number []
13+ dashIndices? : number []
14+ }>
15+ dates: number [],
16+ datetimeFormatterOptions: {
17+ year: string ,
18+ month: string ,
19+ day: string
20+ }
21+ }>()
22+
23+ const { locale } = useI18n ()
24+ const colorMode = useColorMode ()
25+ const resolvedMode = shallowRef <' light' | ' dark' >(' light' )
26+ const rootEl = shallowRef <HTMLElement | null >(null )
27+ const palette = getPalette (' ' )
28+
29+ const step = ref (0 )
30+
31+ onMounted (() => {
32+ rootEl .value = document .documentElement
33+ resolvedMode .value = colorMode .value === ' dark' ? ' dark' : ' light'
34+ })
35+
36+ watch (
37+ () => colorMode .value ,
38+ value => {
39+ resolvedMode .value = value === ' dark' ? ' dark' : ' light'
40+ },
41+ { flush: ' sync' },
42+ )
43+
44+ const { colors } = useCssVariables (
45+ [
46+ ' --bg' ,
47+ ' --fg' ,
48+ ' --bg-subtle' ,
49+ ' --bg-elevated' ,
50+ ' --border-hover' ,
51+ ' --fg-subtle' ,
52+ ' --border' ,
53+ ' --border-subtle' ,
54+ ],
55+ {
56+ element: rootEl ,
57+ watchHtmlAttributes: true ,
58+ watchResize: false , // set to true only if a var changes color on resize
59+ },
60+ )
61+
62+ const isDarkMode = computed (() => resolvedMode .value === ' dark' )
63+
64+ const datasets = computed <VueUiSparklineDatasetItem [][]>(() => {
65+ return (props .dataset ?? []).map ((unit ) => {
66+ return props .dates .map ((period , i ) => {
67+ return {
68+ period ,
69+ value: unit .series [i ] ?? 0 ,
70+ }
71+ })
72+ })
73+ })
74+
75+ const selectedIndex = ref <number | undefined | null >(null )
76+
77+ function hoverIndex({ index }: { index: number | undefined | null }) {
78+ if (typeof index === ' number' ) {
79+ selectedIndex .value = index
80+ }
81+ }
82+
83+ function resetHover() {
84+ selectedIndex .value = null
85+ step .value += 1 // required to reset all chart instances
86+ }
87+
88+ const configs = computed (() => {
89+ return (props .dataset || []).map <VueUiSparklineConfig >((unit , i ) => {
90+ return {
91+ a11y: {
92+ translations: {
93+ keyboardNavigation: $t (
94+ ' package.trends.chart_assistive_text.keyboard_navigation_horizontal' ,
95+ ),
96+ tableAvailable: $t (' package.trends.chart_assistive_text.table_available' ),
97+ tableCaption: $t (' package.trends.chart_assistive_text.table_caption' ),
98+ },
99+ },
100+ theme: isDarkMode .value ? ' dark' : ' ' ,
101+ skeletonConfig: {
102+ style: {
103+ backgroundColor: ' transparent' ,
104+ dataLabel: {
105+ show: true ,
106+ color: ' transparent' ,
107+ },
108+ area: {
109+ color: colors .value .borderHover ,
110+ useGradient: false ,
111+ opacity: 10 ,
112+ },
113+ line: {
114+ color: colors .value .borderHover ,
115+ },
116+ },
117+ },
118+ skeletonDataset: Array .from ({ length: unit .series .length }, () => 0 ),
119+ style: {
120+ backgroundColor: ' transparent' ,
121+ animation: { show: false },
122+ area: {
123+ color: colors .value .borderHover ,
124+ useGradient: false ,
125+ opacity: 10 ,
126+ },
127+ dataLabel: {
128+ offsetX: - 12 ,
129+ fontSize: 24 ,
130+ bold: false ,
131+ color: colors .value .fg ,
132+ datetimeFormatter: {
133+ enable: true ,
134+ locale: locale .value ,
135+ useUTC: true ,
136+ options: props .datetimeFormatterOptions ,
137+ },
138+ },
139+ line: {
140+ color: unit .color ?? palette [i ],
141+ },
142+ plot: {
143+ radius: 6 ,
144+ stroke: isDarkMode .value ? ' oklch(0.985 0 0)' : ' oklch(0.145 0 0)' ,
145+ },
146+ title: {
147+ fontSize: 12 ,
148+ color: colors .value .fgSubtle ,
149+ bold: false ,
150+ },
151+
152+ verticalIndicator: {
153+ strokeDasharray: 5 ,
154+ color: colors .value .fgSubtle ,
155+ },
156+ padding: {
157+ left: 0 ,
158+ right: 0 ,
159+ top: 0 ,
160+ bottom: 0 ,
161+ },
162+ },
163+ }
164+ })
165+ })
166+ </script >
167+
168+ <template >
169+ <div >
170+ <div class =" grid gap-8 sm:grid-cols-2" >
171+ <ClientOnly v-for =" (config, i) in configs" :key =" `config_${i}`" >
172+ <div @mouseleave =" resetHover" class =" w-full max-w-[400px] mx-auto" >
173+ <div class =" flex gap-2 place-items-center" >
174+ <div class =" h-3 w-3" >
175+ <svg viewBox =" 0 0 2 2" class =" w-full" >
176+ <rect x =" 0" y =" 0" width =" 2" height =" 2" rx =" 0.3" :fill =" dataset?.[i]?.color ?? palette[i]" />
177+ </svg >
178+ </div >
179+ {{ applyEllipsis(dataset?.[i]?.name ?? '', 28) }}
180+ </div >
181+ <VueUiSparkline
182+ :key =" `${i}_${step}`"
183+ :config
184+ :dataset =" datasets?.[i]"
185+ :selectedIndex
186+ @hoverIndex =" hoverIndex"
187+ >
188+ <!-- Keyboard navigation hint -->
189+ <template #hint =" { isVisible } " >
190+ <p v-if =" isVisible" class =" text-accent text-xs text-center mt-2" aria-hidden =" true" >
191+ {{ $t('package.downloads.sparkline_nav_hint') }}
192+ </p >
193+ </template >
194+
195+ <template #skeleton >
196+ <!-- This empty div overrides the default built-in scanning animation on load -->
197+ <div />
198+ </template >
199+ </VueUiSparkline >
200+ </div >
201+
202+ <template #fallback >
203+ <!-- Skeleton matching VueUiSparkline layout (title 24px + SVG aspect 500:80) -->
204+ <div class =" max-w-xs" >
205+ <!-- Title row: fontSize * 2 = 24px -->
206+ <div class =" h-6 flex items-center ps-3" >
207+ <SkeletonInline class =" h-3 w-36" />
208+ </div >
209+ <!-- Chart area: matches SVG viewBox 500:80 -->
210+ <div class =" aspect-[500/80] flex items-center" >
211+ <!-- Data label (covers ~42% width, matching dataLabel.offsetX) -->
212+ <div class =" w-[42%] flex items-center ps-0.5" >
213+ <SkeletonInline class =" h-7 w-24" />
214+ </div >
215+ <!-- Sparkline line placeholder -->
216+ <div class =" flex-1 flex items-end pe-3" >
217+ <SkeletonInline class =" h-px w-full" />
218+ </div >
219+ </div >
220+ </div >
221+ </template >
222+ </ClientOnly >
223+ </div >
224+ </div >
225+ </template >
0 commit comments