Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions app/components/Chart/SplitSparkline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type VueUiSparklineDatasetItem,
} from 'vue-data-ui/vue-ui-sparkline'
import { VueUiPatternSeed } from 'vue-data-ui/vue-ui-pattern-seed'
import { useCssVariables } from '~/composables/useColors'
import { useColors } from '~/composables/useColors'
import type { VueUiXyDatasetItem } from 'vue-data-ui/vue-ui-xy'
import { getPalette, lightenColor } from 'vue-data-ui/utils'
import { CHART_PATTERN_CONFIG } from '~/utils/charts'
Expand Down Expand Up @@ -50,23 +50,7 @@ watch(
{ flush: 'sync', immediate: true },
)

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--border-hover',
'--fg-subtle',
'--border',
'--border-subtle',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false, // set to true only if a var changes color on resize
},
)
const { colors } = useColors(rootEl)

const isDarkMode = computed(() => resolvedMode.value === 'dark')

Expand Down
19 changes: 2 additions & 17 deletions app/components/Compare/FacetBarChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { VueUiPatternSeed } from 'vue-data-ui/vue-ui-pattern-seed'
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
import { createPatternDef } from 'vue-data-ui/utils'
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
import { useColors } from '~/composables/useColors'

import {
loadFile,
Expand Down Expand Up @@ -40,23 +41,7 @@ const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpoin

const chartKey = ref(0)

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--fg-subtle',
'--fg-muted',
'--border',
'--border-subtle',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false,
},
)
const { colors } = useColors(rootEl)

const watermarkColors = computed(() => ({
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,
Expand Down
21 changes: 2 additions & 19 deletions app/components/Compare/FacetScatterChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from 'vue-data-ui/vue-ui-scatter'
import { buildCompareScatterChartDataset } from '~/utils/compare-scatter-chart'
import { loadFile, copyAltTextForCompareScatterChart } from '~/utils/charts'
import { useColors } from '~/composables/useColors'

import('vue-data-ui/style.css')

Expand All @@ -26,25 +27,7 @@ const { copy, copied } = useClipboard()
const mobileBreakpointWidth = 640
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--fg-subtle',
'--fg-muted',
'--border',
'--border-subtle',
'--border-hover',
'--accent',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false,
},
)
const { colors } = useColors(rootEl)

const watermarkColors = computed(() => ({
fg: colors.value.fg ?? OKLCH_NEUTRAL_FALLBACK,
Expand Down
20 changes: 2 additions & 18 deletions app/components/Package/TimelineChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import type { TimelineVersion, SubEvent } from '~~/server/api/registry/timeline/[...pkg].get'
import { drawSmallNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
import { useChartTooltipPosition } from '~/composables/useChartTooltipPosition'
import { useColors } from '~/composables/useColors'

import('vue-data-ui/style.css')

Expand Down Expand Up @@ -198,24 +199,7 @@ onMounted(async () => {
resolvedMode.value = colorMode.value === 'dark' ? 'dark' : 'light'
})

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--fg-subtle',
'--fg-muted',
'--border',
'--border-subtle',
'--accent',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false,
},
)
const { colors } = useColors(rootEl)

watch(
() => colorMode.value,
Expand Down
46 changes: 27 additions & 19 deletions app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { Theme as VueDataUiTheme } from 'vue-data-ui'
import { VueUiXy, type VueUiXyConfig, type VueUiXyDatasetItem } from 'vue-data-ui/vue-ui-xy'
import { useDebounceFn, useElementSize, useTimeoutFn } from '@vueuse/core'
import { useCssVariables } from '~/composables/useColors'
import { useColors } from '~/composables/useColors'
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch, lightenOklch } from '~/utils/colors'
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
import { drawNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
Expand Down Expand Up @@ -92,23 +92,7 @@ onMounted(async () => {
loadMetric(selectedMetric.value)
})

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--fg-subtle',
'--fg-muted',
'--border',
'--border-subtle',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false,
},
)
const { colors } = useColors(rootEl)

watch(
() => colorMode.value,
Expand Down Expand Up @@ -1390,6 +1374,8 @@ watch(

const tooltipPosition = useChartTooltipPosition(chartRef)

const keepZoomState = shallowRef(true)

// VueUiXy chart component configuration
const chartConfig = computed<VueUiXyConfig>(() => {
return {
Expand Down Expand Up @@ -1589,7 +1575,7 @@ const chartConfig = computed<VueUiXyConfig>(() => {
maxWidth: isMobile.value ? 350 : 500,
highlightColor: colors.value.bgElevated,
useResetSlot: true,
keepState: true,
keepState: keepZoomState.value,
minimap: {
show: true,
lineColor: '#FAFAFA',
Expand Down Expand Up @@ -1641,6 +1627,28 @@ const isSparklineLayout = computed({
chartLayout.value = v ? 'split' : 'combined'
},
})

const { start: resetZoomState } = useTimeoutFn(
() => {
keepZoomState.value = true
},
1000,
{ immediate: false },
)

async function resetZoom() {
keepZoomState.value = false
await nextTick()
chartRef.value?.resetZoom?.()
resetZoomState()
}

onMounted(resetZoom)

watch([selectedGranularity, startDate, endDate], async () => {
if (!isMounted.value) return
await resetZoom()
})
</script>

<template>
Expand Down
11 changes: 2 additions & 9 deletions app/components/Package/VersionDistribution.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { VueUiXy, type VueUiXyDatasetItem, type VueUiXyConfig } from 'vue-data-ui/vue-ui-xy'
import { useElementSize } from '@vueuse/core'
import { useCssVariables } from '~/composables/useColors'
import { useColors } from '~/composables/useColors'
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch, lightenHex } from '~/utils/colors'
import {
drawSvgPrintLegend,
Expand Down Expand Up @@ -29,14 +29,7 @@ onMounted(async () => {
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,
},
)
const { colors } = useColors(rootEl)
watch(
() => colorMode.value,
Expand Down
20 changes: 2 additions & 18 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
type VueUiSparklineConfig,
type VueUiSparklineDatasetItem,
} from 'vue-data-ui/vue-ui-sparkline'
import { useCssVariables } from '~/composables/useColors'
import { useColors } from '~/composables/useColors'
import type { WeeklyDataPoint } from '~/types/chart'
import { applyDataCorrection } from '~/utils/chart-data-correction'
import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors'
Expand Down Expand Up @@ -86,23 +86,7 @@ watch(
{ flush: 'sync' },
)

const { colors } = useCssVariables(
[
'--bg',
'--fg',
'--bg-subtle',
'--bg-elevated',
'--border-hover',
'--fg-subtle',
'--border',
'--border-subtle',
],
{
element: rootEl,
watchHtmlAttributes: true,
watchResize: false, // set to true only if a var changes color on resize
},
)
const { colors } = useColors(rootEl)

const isDarkMode = computed(() => resolvedMode.value === 'dark')

Expand Down
79 changes: 30 additions & 49 deletions app/composables/useColors.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { computed, type ComputedRef, type Ref, unref } from 'vue'
import { computed, shallowRef, type ComputedRef, type Ref, type ShallowRef, unref } from 'vue'
import { useMutationObserver, useResizeObserver, useSupported } from '@vueuse/core'

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

type UseCssVariableOptions = {
element?: CssVariableSource
watchResize?: boolean
watchHtmlAttributes?: boolean
}
// Add existing css variables to expose in component scripts
const colorVariables = [
'--accent',
'--bg',
'--bg-elevated',
'--bg-subtle',
'--border',
'--border-hover',
'--border-subtle',
'--fg',
'--fg-muted',
'--fg-subtle',
] as const

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

function resolveElement(element?: CssVariableSource): HTMLElement | null {
function resolveElement(element: CssVariableSource): HTMLElement | null {
if (typeof window === 'undefined' || typeof document === 'undefined') return null
if (!element) return document.documentElement
const resolved = unref(element)
return resolved ?? document.documentElement
}

/**
* Read multiple CSS custom properties at once and expose them as a reactive object.
*
* Each CSS variable name is normalized into a camelCase key:
* - Leading `--` is removed
* - kebab-case is converted to camelCase
*
* Example:
* ```ts
* useCssVariables(['--bg', '--fg-subtle'])
* // => colors.value = { bg: '...', fgSubtle: '...' }
* ```
*
* The returned values are always resolved via `getComputedStyle`, meaning the
* effective value is returned (after cascade, theme classes, etc.).
*
* Reactivity behavior:
* - Updates automatically when the observed element changes
* - Can react to theme toggles via `watchHtmlAttributes`
* - Can react to responsive CSS variables via `watchResize`
*
* @param variables - List of CSS variable names (must include the leading `--`)
* @param options - Configuration options
* @param options.element - Element to read variables from (defaults to `:root`)
* @param options.watchResize - Re-evaluate values on resize (useful for media-query-driven variables)
* @param options.watchHtmlAttributes - Re-evaluate values when `<html>` attributes change
*
* @returns An object containing a reactive `colors` map, keyed by camelCase names
*/
export function useCssVariables(
variables: readonly string[],
options: UseCssVariableOptions = {},
export function useColors(
element: ShallowRef<HTMLElement | null, HTMLElement | null>,
options: { watchHtmlAttributes?: boolean; watchResize?: boolean } = {},
): { colors: ComputedRef<Record<string, string>> } {
const recomputeToken = shallowRef(0)
const invalidateColors = () => {
recomputeToken.value += 1
}

const isClientSupported = useSupported(
() => typeof window !== 'undefined' && typeof document !== 'undefined',
)

const elementComputed = computed(() => resolveElement(options.element))

const colors = computed<Record<string, string>>(() => {
const element = elementComputed.value
if (!element) return {}

void recomputeToken.value
const resolvedElement = resolveElement(element)
if (!resolvedElement) return {}
const result: Record<string, string> = {}
for (const variable of variables) {
result[toCamelCase(variable)] = readCssVariable(element, variable)
for (const variable of colorVariables) {
result[toCamelCase(variable)] = readCssVariable(resolvedElement, variable)
}
return result
})

if (options.watchResize) {
useResizeObserver(elementComputed, () => void colors.value)
useResizeObserver(element, invalidateColors)
}

if (options.watchHtmlAttributes && isClientSupported.value) {
useMutationObserver(document.documentElement, () => void colors.value, {
useMutationObserver(document.documentElement, invalidateColors, {
attributes: true,
attributeFilter: ['class', 'style', 'data-theme', 'data-bg-theme'],
})
Expand Down
Loading
Loading