Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,98 @@ Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<div class="bg-white">
<div class="bg-white border-b border-neutral-200">
<div class="container">
<div class="flex items-center gap-3 overflow-x-auto py-5">
<lfx-menu-button
v-for="link of lfCollectionAggregateLinks"
:key="link.key"
:to="{ name: link.routeName, params: { slug: props.slug } }"
:exact="true"
>
<template #default="{ isActive }">
<lfx-icon
:name="link.icon"
:type="isActive ? 'solid' : 'light'"
/>
{{ link.label }}
</template>
</lfx-menu-button>
<div class="flex items-center justify-between gap-3 py-5">
<!-- Desktop: full tab row. Matches project-menu.vue's lg: breakpoint (1024px) so
collection and project pages collapse to the dropdown fallback at the same width. -->
<div class="lg:flex hidden items-center gap-3">
<lfx-menu-button
v-for="link of lfCollectionAggregateLinks"
:key="link.key"
:to="linkTo(link)"
:exact="true"
>
<template #default="{ isActive }">
<lfx-icon
:name="link.icon"
:type="isActive ? 'solid' : 'light'"
/>
{{ link.label }}
</template>
</lfx-menu-button>
</div>
<!-- Tablet/mobile: dropdown fallback, same pattern as project-menu.vue -->
<div class="lg:hidden block min-w-0">
<lfx-dropdown
placement="bottom-start"
width="15rem"
>
<template #trigger>
<lfx-dropdown-selector>
<lfx-icon
:name="activeLink?.icon || ''"
:size="16"
class="text-brand-500 font-black"
/>
{{ activeLink?.label }}
</lfx-dropdown-selector>
</template>

<router-link
v-for="link of lfCollectionAggregateLinks"
:key="link.key"
:to="linkTo(link)"
>
<lfx-dropdown-item
:value="link.key"
:label="link.label"
/>
</router-link>
</lfx-dropdown>
</div>
<lfx-project-date-range-picker
v-if="showDateRangePicker"
class="shrink-0"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'nuxt/app';
import LfxIcon from '~/components/uikit/icon/icon.vue';
import LfxMenuButton from '~/components/uikit/menu-button/menu-button.vue';
import LfxDropdown from '~/components/uikit/dropdown/dropdown.vue';
import LfxDropdownSelector from '~/components/uikit/dropdown/dropdown-selector.vue';
import LfxDropdownItem from '~/components/uikit/dropdown/dropdown-item.vue';
import LfxProjectDateRangePicker from '~/components/modules/project/components/shared/header/date-range-picker.vue';
import { lfCollectionAggregateLinks } from '~/components/modules/collection/config/collection-links';

const props = defineProps<{
slug: string;
showDateRangePicker?: boolean;
}>();

const route = useRoute();
const activeLink = computed(
() => lfCollectionAggregateLinks.find((link) => link.routeName === route.name) || lfCollectionAggregateLinks[0],
);

// Preserve the current query (date range: timeRange/start/end, plus onlyLFProjects) when
// switching tabs, so the selected date range doesn't reset to the default on every tab change.
// `widget` is dropped because it's a per-tab scroll target, not shared across tabs - mirrors
// project-menu.vue's linkUrl().
const linkTo = (link: (typeof lfCollectionAggregateLinks)[number]) => ({
name: link.routeName,
params: { slug: props.slug },
query: {
...route.query,
widget: undefined,
},
});
</script>

<script lang="ts">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ SPDX-License-Identifier: MIT
<template v-if="loading">
<lfx-skeleton
v-for="i in 3"
:key="i"
:key="`skeleton-${i}`"
height="2rem"
width="9rem"
class="rounded-full"
/>
</template>
<template v-else>
<lfx-chip
key="projects-repositories"
type="bordered"
size="small"
class="flex items-center gap-1"
Expand All @@ -32,6 +33,7 @@ SPDX-License-Identifier: MIT
</lfx-chip>

<lfx-chip
key="contributors"
type="bordered"
size="small"
class="flex items-center gap-1"
Expand All @@ -48,6 +50,7 @@ SPDX-License-Identifier: MIT
</lfx-chip>

<lfx-chip
key="avg-health"
type="bordered"
size="small"
class="flex items-center gap-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ Copyright (c) 2025 The Linux Foundation and each contributor.
SPDX-License-Identifier: MIT
-->
<template>
<div :style="headerBackgroundStyle">
<div
:style="headerBackgroundStyle"
class="z-50"
>
<section
class="container pt-3 md:pt-5 pb-3"
:class="scrollTop > 50 ? 'md:pb-5' : 'md:pb-12'"
:class="scrollTop > 250 ? 'md:pb-5' : 'md:pb-12'"
>
<!-- Mobile top row: back link + action icons -->
<div
class="flex md:hidden items-center justify-between transition-all"
:class="scrollTop > 50 ? 'mb-0' : 'mb-4'"
:class="scrollTop > 250 ? 'mb-0' : 'mb-4'"
>
<nuxt-link
:to="{ name: collectionTab?.route }"
Expand All @@ -23,7 +26,7 @@ SPDX-License-Identifier: MIT
/>
<span
class="text-sm font-medium transition-all"
:class="scrollTop > 50 ? 'hidden' : 'block'"
:class="scrollTop > 250 ? 'hidden' : 'block'"
>
{{ collectionTab?.detailsLabel }}
</span>
Expand Down Expand Up @@ -90,7 +93,7 @@ SPDX-License-Identifier: MIT

<!-- Desktop back link (expanded state: its own row above the title) -->
<div
v-if="scrollTop <= 50"
v-if="scrollTop <= 250"
class="transition-all hidden md:block mb-6"
>
<nuxt-link
Expand All @@ -114,12 +117,12 @@ SPDX-License-Identifier: MIT
<div
class="flex-grow flex w-full"
:class="
scrollTop > 50 ? 'flex-row gap-3 items-center' : 'flex-col md:flex-row md:items-stretch gap-3 md:gap-8'
scrollTop > 250 ? 'flex-row gap-3 items-center' : 'flex-col md:flex-row md:items-stretch gap-3 md:gap-8'
"
>
<!-- Compact-only: back arrow inline with title, vertically centered -->
<nuxt-link
v-if="scrollTop > 50"
v-if="scrollTop > 250"
:to="{ name: collectionTab?.route }"
class="hidden md:flex items-center shrink-0"
>
Expand All @@ -132,7 +135,7 @@ SPDX-License-Identifier: MIT
<div
v-if="loading || props.collection?.logoUrl"
class="shrink-0 flex items-center justify-start"
:class="scrollTop > 50 ? 'h-8 md:h-10' : 'h-12 md:h-30'"
:class="scrollTop > 250 ? 'h-8 md:h-10' : 'h-12 md:h-30'"
>
<img
v-if="props.collection?.logoUrl"
Expand All @@ -142,18 +145,18 @@ SPDX-License-Identifier: MIT
height="120"
fetchpriority="high"
decoding="async"
:class="scrollTop > 50 ? 'h-8 w-8 md:h-10 md:w-10' : 'h-12 md:h-30 w-auto'"
:class="scrollTop > 250 ? 'h-8 w-8 md:h-10 md:w-10' : 'h-12 md:h-30 w-auto'"
/>
<lfx-skeleton
v-else
:class="scrollTop > 50 ? 'h-8 w-8 md:h-10 md:w-10' : 'h-12 w-12 md:h-30 md:w-30'"
:class="scrollTop > 250 ? 'h-8 w-8 md:h-10 md:w-10' : 'h-12 w-12 md:h-30 md:w-30'"
class="rounded-md"
/>
</div>
<div class="w-full flex flex-col justify-center min-w-0">
<!-- Mobile only: visibility badge above title for my-collections -->
<div
v-if="props.type === CollectionTypeEnum.MY_COLLECTIONS && props.collection && scrollTop <= 50"
v-if="props.type === CollectionTypeEnum.MY_COLLECTIONS && props.collection && scrollTop <= 250"
class="flex md:hidden items-center gap-1.5 mb-2"
>
<lfx-icon
Expand All @@ -172,13 +175,13 @@ SPDX-License-Identifier: MIT
v-if="loading"
width="80%"
class="rounded-sm"
:class="scrollTop > 50 ? 'h-6 md:h-9' : 'h-9 md:h-13'"
:class="scrollTop > 250 ? 'h-6 md:h-9' : 'h-9 md:h-13'"
/>
<h1
v-else-if="props.collection"
class="font-secondary font-light transition-all text-neutral-900"
:class="
scrollTop > 50
scrollTop > 250
? 'text-lg md:text-2xl md:leading-9 truncate'
: 'text-3xl md:text-4xl md:leading-[56px]'
"
Expand All @@ -194,7 +197,7 @@ SPDX-License-Identifier: MIT
class="hidden md:flex transition-all ease-linear items-center gap-4 lg:w-auto shrink-0 mt-4 lg:mt-0"
>
<lfx-toggle
v-if="scrollTop > 50"
v-if="scrollTop > 250 && isProjectsTab"
v-model="isOnlyLFProjects"
>
Only Linux Foundation projects
Expand Down Expand Up @@ -227,7 +230,7 @@ SPDX-License-Identifier: MIT
class="!rounded-full shadow-sm"
/>
<lfx-button
v-if="scrollTop <= 50"
v-if="scrollTop <= 250"
type="outline"
class="!rounded-full shadow-sm"
@click="handleShare"
Expand Down Expand Up @@ -272,7 +275,7 @@ SPDX-License-Identifier: MIT
<!-- Loading placeholder keeps meta-row height reserved so data resolution doesn't reflow the page -->
<div
v-if="loading"
:class="scrollTop > 50 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-3 md:mt-10'"
:class="scrollTop > 250 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-3 md:mt-10'"
class="flex items-center gap-2 w-full transition-all ease-linear"
>
<lfx-skeleton
Expand All @@ -284,7 +287,7 @@ SPDX-License-Identifier: MIT
<!-- Owner + project count + LF toggle (desktop only for toggle) -->
<div
v-if="!loading && props.collection"
:class="scrollTop > 50 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-2'"
:class="scrollTop > 250 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-2'"
class="flex items-center gap-2 justify-between w-full flex-wrap transition-all ease-linear"
>
<div class="flex items-center gap-1 md:gap-2 flex-wrap">
Expand Down Expand Up @@ -319,7 +322,7 @@ SPDX-License-Identifier: MIT

<!-- Description paragraph -->
<div
:class="scrollTop > 50 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-1 md:mt-3'"
:class="scrollTop > 250 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-1 md:mt-3'"
class="w-full transition-all ease-linear"
>
<div
Expand Down Expand Up @@ -348,15 +351,15 @@ SPDX-License-Identifier: MIT
<!-- Aggregate metrics row + LF toggle (desktop only for toggle) -->
<div
v-if="showMetricsRow"
:class="scrollTop > 50 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-10'"
:class="scrollTop > 250 ? 'h-0 opacity-0 invisible pt-0' : 'h-auto opacity-100 visible mt-10'"
class="w-full flex items-center justify-between gap-2 flex-wrap transition-all ease-linear"
>
<lfx-collection-metrics-row
:metrics="props.metrics"
:loading="props.metricsLoading"
/>
<lfx-toggle
v-if="scrollTop <= 50"
v-if="scrollTop <= 250 && isProjectsTab"
v-model="isOnlyLFProjects"
class="!hidden md:!flex"
>
Expand Down Expand Up @@ -385,7 +388,7 @@ SPDX-License-Identifier: MIT
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useRouter } from 'nuxt/app';
import { useRoute, useRouter } from 'nuxt/app';
import { useQueryClient } from '@tanstack/vue-query';
import { collectionTabs, headerBackground, CollectionTypeEnum } from '../../config/collection-type-config';
import LfxCollectionMetricsRow from './collection-metrics-row.vue';
Expand Down Expand Up @@ -425,8 +428,14 @@ const authStore = useAuthStore();
const { user } = storeToRefs(authStore);

const router = useRouter();
const route = useRoute();
const { openShareModal } = useShareStore();

// The LF-projects toggle filters the Projects tab's table - it has no effect on the
// Contributors/Popularity/Development tabs, which show collection-wide aggregate widgets
// instead of a filterable project list, so it's hidden there.
const isProjectsTab = computed(() => route.name === LfxRoutes.COLLECTION);

const props = defineProps<{
collection?: Collection;
loading?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ export interface CollectionLinkConfig {

export const lfCollectionAggregateLinks: CollectionLinkConfig[] = [
{ key: 'projects', icon: 'layer-group', label: 'Projects', routeName: LfxRoutes.COLLECTION },
// {
// key: 'contributors',
// icon: 'people-group',
// label: 'Contributors',
// routeName: LfxRoutes.COLLECTION_CONTRIBUTORS,
// },
// {
// key: 'popularity',
// icon: 'fire',
// label: 'Popularity',
// routeName: LfxRoutes.COLLECTION_POPULARITY,
// },
// {
// key: 'development',
// icon: 'code',
// label: 'Development',
// routeName: LfxRoutes.COLLECTION_DEVELOPMENT,
// },
{
key: 'contributors',
icon: 'people-group',
label: 'Contributors',
routeName: LfxRoutes.COLLECTION_CONTRIBUTORS,
},
{
key: 'popularity',
icon: 'fire',
label: 'Popularity',
routeName: LfxRoutes.COLLECTION_POPULARITY,
},
{
key: 'development',
icon: 'code',
label: 'Development',
routeName: LfxRoutes.COLLECTION_DEVELOPMENT,
},
];
Loading
Loading