Skip to content

Commit 4269128

Browse files
authored
refactor: useColors composable for charts (#2696)
1 parent 3033687 commit 4269128

10 files changed

Lines changed: 129 additions & 270 deletions

File tree

app/components/Chart/SplitSparkline.vue

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type VueUiSparklineDatasetItem,
66
} from 'vue-data-ui/vue-ui-sparkline'
77
import { VueUiPatternSeed } from 'vue-data-ui/vue-ui-pattern-seed'
8-
import { useCssVariables } from '~/composables/useColors'
8+
import { useColors } from '~/composables/useColors'
99
import type { VueUiXyDatasetItem } from 'vue-data-ui/vue-ui-xy'
1010
import { getPalette, lightenColor } from 'vue-data-ui/utils'
1111
import { CHART_PATTERN_CONFIG } from '~/utils/charts'
@@ -50,23 +50,7 @@ watch(
5050
{ flush: 'sync', immediate: true },
5151
)
5252
53-
const { colors } = useCssVariables(
54-
[
55-
'--bg',
56-
'--fg',
57-
'--bg-subtle',
58-
'--bg-elevated',
59-
'--border-hover',
60-
'--fg-subtle',
61-
'--border',
62-
'--border-subtle',
63-
],
64-
{
65-
element: rootEl,
66-
watchHtmlAttributes: true,
67-
watchResize: false, // set to true only if a var changes color on resize
68-
},
69-
)
53+
const { colors } = useColors(rootEl)
7054
7155
const isDarkMode = computed(() => resolvedMode.value === 'dark')
7256

app/components/Compare/FacetBarChart.vue

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { VueUiPatternSeed } from 'vue-data-ui/vue-ui-pattern-seed'
99
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
1010
import { createPatternDef } from 'vue-data-ui/utils'
1111
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
12+
import { useColors } from '~/composables/useColors'
1213
1314
import {
1415
loadFile,
@@ -40,23 +41,7 @@ const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpoin
4041
4142
const chartKey = ref(0)
4243
43-
const { colors } = useCssVariables(
44-
[
45-
'--bg',
46-
'--fg',
47-
'--bg-subtle',
48-
'--bg-elevated',
49-
'--fg-subtle',
50-
'--fg-muted',
51-
'--border',
52-
'--border-subtle',
53-
],
54-
{
55-
element: rootEl,
56-
watchHtmlAttributes: true,
57-
watchResize: false,
58-
},
59-
)
44+
const { colors } = useColors(rootEl)
6045
6146
const watermarkColors = computed(() => ({
6247
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,

app/components/Compare/FacetScatterChart.vue

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from 'vue-data-ui/vue-ui-scatter'
1010
import { buildCompareScatterChartDataset } from '~/utils/compare-scatter-chart'
1111
import { loadFile, copyAltTextForCompareScatterChart } from '~/utils/charts'
12+
import { useColors } from '~/composables/useColors'
1213
1314
import('vue-data-ui/style.css')
1415
@@ -26,25 +27,7 @@ const { copy, copied } = useClipboard()
2627
const mobileBreakpointWidth = 640
2728
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)
2829
29-
const { colors } = useCssVariables(
30-
[
31-
'--bg',
32-
'--fg',
33-
'--bg-subtle',
34-
'--bg-elevated',
35-
'--fg-subtle',
36-
'--fg-muted',
37-
'--border',
38-
'--border-subtle',
39-
'--border-hover',
40-
'--accent',
41-
],
42-
{
43-
element: rootEl,
44-
watchHtmlAttributes: true,
45-
watchResize: false,
46-
},
47-
)
30+
const { colors } = useColors(rootEl)
4831
4932
const watermarkColors = computed(() => ({
5033
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,

app/components/Package/TimelineChart.vue

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import type { TimelineVersion, SubEvent } from '~~/server/api/registry/timeline/[...pkg].get'
2020
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
2121
import { useChartTooltipPosition } from '~/composables/useChartTooltipPosition'
22+
import { useColors } from '~/composables/useColors'
2223
2324
import('vue-data-ui/style.css')
2425
@@ -198,24 +199,7 @@ onMounted(async () => {
198199
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
199200
})
200201
201-
const { colors } = useCssVariables(
202-
[
203-
'--bg',
204-
'--fg',
205-
'--bg-subtle',
206-
'--bg-elevated',
207-
'--fg-subtle',
208-
'--fg-muted',
209-
'--border',
210-
'--border-subtle',
211-
'--accent',
212-
],
213-
{
214-
element: rootEl,
215-
watchHtmlAttributes: true,
216-
watchResize: false,
217-
},
218-
)
202+
const { colors } = useColors(rootEl)
219203
220204
watch(
221205
() => colorMode.value,

app/components/Package/TrendsChart.vue

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { Theme as VueDataUiTheme } from 'vue-data-ui'
33
import { VueUiXy, type VueUiXyConfig, type VueUiXyDatasetItem } from 'vue-data-ui/vue-ui-xy'
44
import { useDebounceFn, useElementSize, useTimeoutFn } from '@vueuse/core'
5-
import { useCssVariables } from '~/composables/useColors'
5+
import { useColors } from '~/composables/useColors'
66
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch, lightenOklch } from '~/utils/colors'
77
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
88
import { drawNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
@@ -92,23 +92,7 @@ onMounted(async () => {
9292
loadMetric(selectedMetric.value)
9393
})
9494
95-
const { colors } = useCssVariables(
96-
[
97-
'--bg',
98-
'--fg',
99-
'--bg-subtle',
100-
'--bg-elevated',
101-
'--fg-subtle',
102-
'--fg-muted',
103-
'--border',
104-
'--border-subtle',
105-
],
106-
{
107-
element: rootEl,
108-
watchHtmlAttributes: true,
109-
watchResize: false,
110-
},
111-
)
95+
const { colors } = useColors(rootEl)
11296
11397
watch(
11498
() => colorMode.value,
@@ -1390,6 +1374,8 @@ watch(
13901374
13911375
const tooltipPosition = useChartTooltipPosition(chartRef)
13921376
1377+
const keepZoomState = shallowRef(true)
1378+
13931379
// VueUiXy chart component configuration
13941380
const chartConfig = computed<VueUiXyConfig>(() => {
13951381
return {
@@ -1589,7 +1575,7 @@ const chartConfig = computed<VueUiXyConfig>(() => {
15891575
maxWidth: isMobile.value ? 350 : 500,
15901576
highlightColor: colors.value.bgElevated,
15911577
useResetSlot: true,
1592-
keepState: true,
1578+
keepState: keepZoomState.value,
15931579
minimap: {
15941580
show: true,
15951581
lineColor: '#FAFAFA',
@@ -1641,6 +1627,28 @@ const isSparklineLayout = computed({
16411627
chartLayout.value = v ? 'split' : 'combined'
16421628
},
16431629
})
1630+
1631+
const { start: resetZoomState } = useTimeoutFn(
1632+
() => {
1633+
keepZoomState.value = true
1634+
},
1635+
1000,
1636+
{ immediate: false },
1637+
)
1638+
1639+
async function resetZoom() {
1640+
keepZoomState.value = false
1641+
await nextTick()
1642+
chartRef.value?.resetZoom?.()
1643+
resetZoomState()
1644+
}
1645+
1646+
onMounted(resetZoom)
1647+
1648+
watch([selectedGranularity, startDate, endDate], async () => {
1649+
if (!isMounted.value) return
1650+
await resetZoom()
1651+
})
16441652
</script>
16451653

16461654
<template>

app/components/Package/VersionDistribution.vue

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { VueUiXy, type VueUiXyDatasetItem, type VueUiXyConfig } from 'vue-data-ui/vue-ui-xy'
33
import { useElementSize } from '@vueuse/core'
4-
import { useCssVariables } from '~/composables/useColors'
4+
import { useColors } from '~/composables/useColors'
55
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch, lightenHex } from '~/utils/colors'
66
import {
77
drawSvgPrintLegend,
@@ -29,14 +29,7 @@ onMounted(async () => {
2929
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
3030
})
3131
32-
const { colors } = useCssVariables(
33-
['--bg', '--fg', '--bg-subtle', '--bg-elevated', '--fg-subtle', '--border', '--border-subtle'],
34-
{
35-
element: rootEl,
36-
watchHtmlAttributes: true,
37-
watchResize: false,
38-
},
39-
)
32+
const { colors } = useColors(rootEl)
4033
4134
watch(
4235
() => colorMode.value,

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
type VueUiSparklineConfig,
55
type VueUiSparklineDatasetItem,
66
} from 'vue-data-ui/vue-ui-sparkline'
7-
import { useCssVariables } from '~/composables/useColors'
7+
import { useColors } from '~/composables/useColors'
88
import type { WeeklyDataPoint } from '~/types/chart'
99
import { applyDataCorrection } from '~/utils/chart-data-correction'
1010
import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors'
@@ -86,23 +86,7 @@ watch(
8686
{ flush: 'sync' },
8787
)
8888
89-
const { colors } = useCssVariables(
90-
[
91-
'--bg',
92-
'--fg',
93-
'--bg-subtle',
94-
'--bg-elevated',
95-
'--border-hover',
96-
'--fg-subtle',
97-
'--border',
98-
'--border-subtle',
99-
],
100-
{
101-
element: rootEl,
102-
watchHtmlAttributes: true,
103-
watchResize: false, // set to true only if a var changes color on resize
104-
},
105-
)
89+
const { colors } = useColors(rootEl)
10690
10791
const isDarkMode = computed(() => resolvedMode.value === 'dark')
10892

app/composables/useColors.ts

Lines changed: 30 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import { computed, type ComputedRef, type Ref, unref } from 'vue'
1+
import { computed, shallowRef, type ComputedRef, type Ref, type ShallowRef, unref } from 'vue'
22
import { useMutationObserver, useResizeObserver, useSupported } from '@vueuse/core'
33

44
type CssVariableSource = HTMLElement | null | undefined | Ref<HTMLElement | null | undefined>
55

6-
type UseCssVariableOptions = {
7-
element?: CssVariableSource
8-
watchResize?: boolean
9-
watchHtmlAttributes?: boolean
10-
}
6+
// Add existing css variables to expose in component scripts
7+
const colorVariables = [
8+
'--accent',
9+
'--bg',
10+
'--bg-elevated',
11+
'--bg-subtle',
12+
'--border',
13+
'--border-hover',
14+
'--border-subtle',
15+
'--fg',
16+
'--fg-muted',
17+
'--fg-subtle',
18+
] as const
1119

1220
function readCssVariable(element: HTMLElement, variableName: string): string {
1321
return getComputedStyle(element).getPropertyValue(variableName).trim()
@@ -17,69 +25,42 @@ function toCamelCase(cssVariable: string): string {
1725
return cssVariable.replace(/^--/, '').replace(/-([a-z0-9])/gi, (_, c) => c.toUpperCase())
1826
}
1927

20-
function resolveElement(element?: CssVariableSource): HTMLElement | null {
28+
function resolveElement(element: CssVariableSource): HTMLElement | null {
2129
if (typeof window === 'undefined' || typeof document === 'undefined') return null
22-
if (!element) return document.documentElement
2330
const resolved = unref(element)
2431
return resolved ?? document.documentElement
2532
}
2633

27-
/**
28-
* Read multiple CSS custom properties at once and expose them as a reactive object.
29-
*
30-
* Each CSS variable name is normalized into a camelCase key:
31-
* - Leading `--` is removed
32-
* - kebab-case is converted to camelCase
33-
*
34-
* Example:
35-
* ```ts
36-
* useCssVariables(['--bg', '--fg-subtle'])
37-
* // => colors.value = { bg: '...', fgSubtle: '...' }
38-
* ```
39-
*
40-
* The returned values are always resolved via `getComputedStyle`, meaning the
41-
* effective value is returned (after cascade, theme classes, etc.).
42-
*
43-
* Reactivity behavior:
44-
* - Updates automatically when the observed element changes
45-
* - Can react to theme toggles via `watchHtmlAttributes`
46-
* - Can react to responsive CSS variables via `watchResize`
47-
*
48-
* @param variables - List of CSS variable names (must include the leading `--`)
49-
* @param options - Configuration options
50-
* @param options.element - Element to read variables from (defaults to `:root`)
51-
* @param options.watchResize - Re-evaluate values on resize (useful for media-query-driven variables)
52-
* @param options.watchHtmlAttributes - Re-evaluate values when `<html>` attributes change
53-
*
54-
* @returns An object containing a reactive `colors` map, keyed by camelCase names
55-
*/
56-
export function useCssVariables(
57-
variables: readonly string[],
58-
options: UseCssVariableOptions = {},
34+
export function useColors(
35+
element: ShallowRef<HTMLElement | null, HTMLElement | null>,
36+
options: { watchHtmlAttributes?: boolean; watchResize?: boolean } = {},
5937
): { colors: ComputedRef<Record<string, string>> } {
38+
const recomputeToken = shallowRef(0)
39+
const invalidateColors = () => {
40+
recomputeToken.value += 1
41+
}
42+
6043
const isClientSupported = useSupported(
6144
() => typeof window !== 'undefined' && typeof document !== 'undefined',
6245
)
6346

64-
const elementComputed = computed(() => resolveElement(options.element))
65-
6647
const colors = computed<Record<string, string>>(() => {
67-
const element = elementComputed.value
68-
if (!element) return {}
69-
48+
void recomputeToken.value
49+
const resolvedElement = resolveElement(element)
50+
if (!resolvedElement) return {}
7051
const result: Record<string, string> = {}
71-
for (const variable of variables) {
72-
result[toCamelCase(variable)] = readCssVariable(element, variable)
52+
for (const variable of colorVariables) {
53+
result[toCamelCase(variable)] = readCssVariable(resolvedElement, variable)
7354
}
7455
return result
7556
})
7657

7758
if (options.watchResize) {
78-
useResizeObserver(elementComputed, () => void colors.value)
59+
useResizeObserver(element, invalidateColors)
7960
}
8061

8162
if (options.watchHtmlAttributes && isClientSupported.value) {
82-
useMutationObserver(document.documentElement, () => void colors.value, {
63+
useMutationObserver(document.documentElement, invalidateColors, {
8364
attributes: true,
8465
attributeFilter: ['class', 'style', 'data-theme', 'data-bg-theme'],
8566
})

0 commit comments

Comments
 (0)