Skip to content
Open
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
58 changes: 49 additions & 9 deletions app/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@

/* text colors */
--fg: oklch(0.982 0 0);
--fg-muted: oklch(0.749 0 0);
--fg-subtle: oklch(0.673 0 0);
--fg-muted: oklch(0.849 0 0);
--fg-subtle: oklch(0.773 0 0);

/* border, separator colors */
--border: oklch(0.269 0 0);
--border-subtle: oklch(0.239 0 0);
--border-hover: oklch(0.371 0 0);
--border-elevated: oklch(0.369 0 0);

/* accent color, set by user from settings */
--accent: var(--accent-color, oklch(0.787 0.128 230.318));
Expand Down Expand Up @@ -81,19 +82,38 @@
--bg-elevated: oklch(0.264 0 0);
}

:root[data-theme='dark'][data-fg-theme='contrast'] {
--fg: oklch(1 0 0);
--fg-muted: oklch(0.95 0 0);
--fg-subtle: oklch(0.9 0 0);
}

:root[data-theme='dark'][data-fg-theme='standard'] {
--fg: oklch(0.982 0 0);
--fg-muted: oklch(0.849 0 0);
--fg-subtle: oklch(0.773 0 0);
}

:root[data-theme='dark'][data-fg-theme='muted'] {
--fg: oklch(0.969 0 0);
--fg-muted: oklch(0.749 0 0);
--fg-subtle: oklch(0.673 0 0);
}

:root[data-theme='light'] {
--bg: var(--bg-color, oklch(1 0 0));
--bg-subtle: var(--bg-subtle-color, oklch(0.979 0.001 286.375));
--bg-muted: var(--bg-muted-color, oklch(0.955 0.001 286.76));
--bg-elevated: var(--bg-elevated-color, oklch(0.94 0.002 287.29));

--fg: oklch(0.146 0 0);
--fg-muted: oklch(0.398 0 0);
--fg-subtle: oklch(0.48 0 0);
--fg: oklch(0.046 0 0);
--fg-muted: oklch(0.198 0 0);
--fg-subtle: oklch(0.28 0 0);

--border: oklch(0.8514 0 0);
--border-subtle: oklch(0.922 0 0);
--border-hover: oklch(0.715 0 0);
--border-elevated: oklch(0.75 0 0);

--accent: var(--accent-color, oklch(0.5 0.16 247.27));
--accent-muted: var(--accent-color, oklch(0.205 0 0));
Expand Down Expand Up @@ -147,29 +167,49 @@
--bg-subtle: var(--bg-subtle-color, oklch(0.979 0 0));
}

:root[data-theme='light'][data-fg-theme='contrast'] {
--fg: oklch(0 0 0);
--fg-muted: oklch(0.1 0 0);
--fg-subtle: oklch(0.2 0 0);
}

:root[data-theme='light'][data-fg-theme='standard'] {
--fg: oklch(0.046 0 0);
--fg-muted: oklch(0.198 0 0);
--fg-subtle: oklch(0.28 0 0);
}

:root[data-theme='light'][data-fg-theme='muted'] {
--fg: oklch(0.146 0 0);
--fg-muted: oklch(0.398 0 0);
--fg-subtle: oklch(0.48 0 0);
}

@media (prefers-contrast: more) {
:root[data-theme='dark'] {
/* text colors */
--fg: oklch(1 0 0);
--fg-muted: oklch(0.769 0 0);
--fg-subtle: oklch(0.693 0 0);
--fg-muted: oklch(0.869 0 0);
--fg-subtle: oklch(0.793 0 0);

/* border, separator colors */
--border: oklch(0.769 0 0);
--border-subtle: oklch(0.739 0 0);
--border-hover: oklch(0.771 0 0);
--border-elevated: oklch(0.869 0 0);
}

:root[data-theme='light'] {
/* text colors */
--fg: oklch(0 0 0);
--fg-muted: oklch(0.329 0 0);
--fg-subtle: oklch(0.4 0 0);
--fg-muted: oklch(0.229 0 0);
--fg-subtle: oklch(0.3 0 0);

/* border, separator colors */
--border: oklch(0.3514 0 0);
--border-subtle: oklch(0.422 0 0);
--border-hover: oklch(0.315 0 0);
--border-elevated: oklch(0.25 0 0);
}
}

Expand Down
4 changes: 2 additions & 2 deletions app/components/Button/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defineExpose({
<template>
<button
ref="el"
class="group gap-x-1 items-center justify-center font-mono border border-border rounded-md transition-all duration-200 cursor-pointer disabled:(opacity-40 cursor-not-allowed border-transparent)"
class="group gap-x-1 items-center justify-center font-mono border border-border-elevated/60 rounded-md transition-all duration-200 cursor-pointer disabled:(opacity-40 cursor-not-allowed border-transparent)"
:class="{
'inline-flex': !block,
'flex': block,
Expand All @@ -60,7 +60,7 @@ defineExpose({
'text-xs p-0.5': size === 'sm' && !!iconOnly,
'px-2': size === 'sm' && !classicon && !iconOnly,
'ps-1.5 pe-2': size === 'sm' && !!classicon && !iconOnly,
'bg-transparent text-fg hover:enabled:(bg-fg/10) focus-visible:enabled:(bg-fg/10) aria-pressed:(bg-fg/10 border-fg/20 hover:enabled:(bg-fg/20 text-fg/50))':
'bg-transparent text-fg hover:enabled:(bg-bg-elevated) focus-visible:enabled:(bg-bg-elevated) aria-pressed:(bg-bg-muted border-border-hover hover:enabled:(bg-bg-muted text-fg/50))':
variant === 'secondary',
'text-bg bg-fg hover:enabled:(bg-fg/50) focus-visible:enabled:(bg-fg/50) aria-pressed:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
variant === 'primary',
Expand Down
2 changes: 1 addition & 1 deletion app/components/Input/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const inputAttrs = computed(() => ({
v-bind="inputAttrs"
@focus="emit('focus', $event)"
@blur="emit('blur', $event)"
class="appearance-none bg-bg-subtle border border-border font-mono text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent outline-offset-2 focus:border-accent focus-visible:outline-accent/70 disabled:(opacity-50 cursor-not-allowed)"
class="appearance-none bg-bg-subtle border border-border-elevated font-mono text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent outline-offset-2 focus:border-accent focus-visible:outline-accent/70 disabled:(opacity-50 cursor-not-allowed)"
:class="{
'text-xs leading-[1.2] px-2 py-2 rounded-md': size === 'sm',
'text-sm leading-none px-3 py-2.5 rounded-lg': size === 'md',
Expand Down
2 changes: 1 addition & 1 deletion app/components/Package/Dependencies.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const numberFormatter = useNumberFormatter()
class="inline-flex items-center justify-center p-2 -m-2"
:aria-label="getOutdatedTooltip(outdatedDeps[dep], $t)"
>
<span class="i-lucide:circle-alert w-3 h-3" aria-hidden="true" />
<span class="i-lucide:arrow-up w-3 h-3" aria-hidden="true" />
</button>
</TooltipApp>
<TooltipApp
Expand Down
45 changes: 35 additions & 10 deletions app/components/Package/TimelineChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,34 @@ const seriesDependencies = computed(() => {
}
})

const activeTab = shallowRef<TimelineChartMetric>('totalSize')
const timelineChartMetrics = new Set<TimelineChartMetric>([
'totalSize',
'dependencyCount',
'dependencySize',
])

const activeTab = usePermalink<TimelineChartMetric>('metric', 'totalSize', {
permanent: true,
})

watch(
activeTab,
value => {
if (!timelineChartMetrics.has(value)) {
activeTab.value = 'totalSize'
}
},
{ immediate: true },
)

const shouldPauseChartAnimations = shallowRef(true)

const { start: startChartAnimationPauseTimer } = useTimeoutFn(
() => {
shouldPauseChartAnimations.value = false
},
1000,
{ immediate: false },
300,
{ immediate: true },
)

function pauseChartAnimations() {
Expand Down Expand Up @@ -859,7 +877,7 @@ const timelineMetricTabs = computed(() => [
<div
style="width: 100%"
class="font-mono border-b border-border"
:class="{ loaded: shouldPauseChartAnimations }"
:class="{ loading: shouldPauseChartAnimations || loading }"
id="timeline-chart"
>
<div class="mt-4 flex flex-row flex-wrap items-center justify-between gap-4">
Expand Down Expand Up @@ -967,7 +985,7 @@ const timelineMetricTabs = computed(() => [
:markersNegative="getNegativeDatapointPlots(svg.data[0], svg.slicer.start)"
:colors
:gradientColors="E18E_GRADIENT_COLORS"
:pauseAnimations="shouldPauseChartAnimations"
:pauseAnimations="shouldPauseChartAnimations || loading"
/>
</template>

Expand Down Expand Up @@ -1040,6 +1058,10 @@ const timelineMetricTabs = computed(() => [
:dataset="datasets.dependencySize"
:config="stackbarConfig"
:selected-x-index="indexSelection"
:style="{
opacity: shouldPauseChartAnimations || loading ? 0 : 1,
transition: 'opacity 0.15s',
}"
ref="chartRef"
>
<!-- Injecting custom svg elements -->
Expand All @@ -1058,7 +1080,7 @@ const timelineMetricTabs = computed(() => [
"
:activeVersionPlot="getActiveVersionDatapointBar(svg.data, svg.barWidth)"
:colors
:pauseAnimations="shouldPauseChartAnimations"
:pauseAnimations="shouldPauseChartAnimations || loading"
/>
</template>

Expand Down Expand Up @@ -1112,7 +1134,10 @@ const timelineMetricTabs = computed(() => [
</VueUiStackbar>

<template #fallback>
<SkeletonBlock class="flex place-items-center justify-center aspect-[1152/254.59]">
<SkeletonBlock
class="flex place-items-center justify-center"
:class="[activeTab === 'dependencySize' ? 'aspect-[1152/466.4]' : 'aspect-[1152/254.59]']"
>
<span class="i-lucide:chart-line w-10 h-10 text-fg-muted" aria-hidden="true" />
</SkeletonBlock>
</template>
Expand Down Expand Up @@ -1198,9 +1223,9 @@ const timelineMetricTabs = computed(() => [
animation: indeterminate 1.5s ease-in-out infinite;
}

.loaded :deep(.vue-data-ui-component .serie_line_0 path),
.loaded :deep(.vdui-shape-circle),
.loaded :deep(.vue-ui-stackbar rect) {
.loading :deep(.vue-data-ui-component .serie_line_0 path),
.loading :deep(.vdui-shape-circle),
.loading :deep(.vue-ui-stackbar rect) {
transition: none !important;
animation: none !important;
}
Expand Down
6 changes: 3 additions & 3 deletions app/components/Package/Versions.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { compare, validRange } from 'semver'
import { compare, normalizeRange } from 'verkit'
import type { RouteLocationRaw } from 'vue-router'
import type { TrustStatus } from 'packumeta'
import { fetchAllPackageVersions } from '~/utils/npm/api'
Expand Down Expand Up @@ -55,7 +55,7 @@ const semverFilter = ref('')
watch(semverFilter, async newFilter => {
const trimmed = newFilter.trim()
if (trimmed === '' || hasLoadedAll.value) return
if (!validRange(trimmed)) return
if (!normalizeRange(trimmed)) return

try {
const allVersions = await loadAllVersions()
Expand Down Expand Up @@ -86,7 +86,7 @@ const filteredVersionSet = computed(() =>
)
const isFilterActive = computed(() => semverFilter.value.trim() !== '')
const isInvalidRange = computed(
() => isFilterActive.value && validRange(semverFilter.value.trim()) === null,
() => isFilterActive.value && normalizeRange(semverFilter.value.trim()) === null,
)

// All tag rows derived from props (SSR-safe)
Expand Down
2 changes: 1 addition & 1 deletion app/components/Select/Base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const props = withDefaults(defineProps<SelectBaseProps>(), {
<template>
<select
v-model="model"
class="bg-bg border border-border font-mono text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent outline-offset-2 focus:border-accent focus-visible:outline-accent/70 disabled:(opacity-50 cursor-not-allowed)"
class="bg-bg border border-border-elevated font-mono text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent outline-offset-2 focus:border-accent focus-visible:outline-accent/70 disabled:(opacity-50 cursor-not-allowed)"
:class="[SELECT_SIZES[size]]"
:disabled="
/** Catching Vue render-bug of invalid `disabled=false` attribute in the final HTML */
Expand Down
67 changes: 67 additions & 0 deletions app/components/Settings/FgThemePicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script setup lang="ts">
const { foregroundThemes, selectedForegroundTheme, setForegroundTheme } = useForegroundTheme()

onPrehydrate(el => {
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
// Hardcoded — onPrehydrate is serialized into a <script> tag and cannot reference imports
const defaultId = 'standard'
const id = settings.preferredForegroundTheme
if (id) {
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
if (input) {
input.checked = true
input.setAttribute('checked', '')
}
// Remove checked from the server-default (clear button, value="")
if (id !== defaultId) {
const clearInput = el.querySelector<HTMLInputElement>(`input[value="${defaultId}"]`)
if (clearInput) {
clearInput.checked = false
clearInput.removeAttribute('checked')
}
}
}
})
</script>

<template>
<fieldset
class="flex items-center gap-4 has-[input:focus-visible]:(outline-solid outline-accent/70 outline-offset-4) rounded-xl w-fit"
>
<legend class="sr-only">{{ $t('settings.foreground_themes.label') }}</legend>
<label
v-for="theme in foregroundThemes"
:key="theme.id"
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
:style="{ backgroundColor: theme.value }"
>
<input
type="radio"
name="foreground-theme"
class="sr-only"
:value="theme.id"
:checked="
selectedForegroundTheme === theme.id ||
(!selectedForegroundTheme && theme.id === 'standard')
"
:aria-label="theme.label"
@change="setForegroundTheme(theme.id)"
/>
</label>
</fieldset>
</template>

<style scoped>
@media (forced-colors: active) {
/* keep background theme swatches visible in forced colors. */
label {
forced-color-adjust: none;
border: 1px solid CanvasText;

&:has(> input:checked) {
outline: 2px solid Highlight;
outline-offset: 2px;
}
}
}
</style>
4 changes: 2 additions & 2 deletions app/components/VersionSelector.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import { compare } from 'semver'
import { compare } from 'verkit'
import {
buildVersionToTagsMap,
getPrereleaseChannel,
Expand Down Expand Up @@ -531,7 +531,7 @@ watch(
type="button"
aria-haspopup="listbox"
:aria-expanded="isOpen"
class="break-all text-start text-fg-subtle font-mono text-sm hover:text-fg transition-[color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg rounded"
class="break-all text-start font-mono text-sm hover:text-accent transition-[color] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg rounded"
@click="isOpen = !isOpen"
@keydown="handleButtonKeydown"
data-testid="version-selector-button"
Expand Down
Loading
Loading