feat(mall): pin home category menu beside hero carousel

Home hero becomes a 450px flex row with a pinned 240x450 category rail
(ShellCategoryMenu `pinned` mode) left of the 960px center-cropped
carousel; hovering a top-level category expands the mega-menu over the
carousel. The header no longer collapses on scroll, and its category
dropdown is now hover-only on non-home pages.

OpenSpec change: openspec/changes/pin-home-category-menu

BREAKING CHANGE: the header logo/search/cart bar no longer auto-hides
past 200px of scroll, and the home page no longer shows the header
category dropdown (it is pinned in the hero row instead).
This commit is contained in:
2026-09-17 14:17:46 +00:00
parent 21e99bb52b
commit ee2f421709
7 changed files with 127 additions and 11 deletions
+28 -1
View File
@@ -2,6 +2,8 @@
import { topCategories, childCategories } from "~/mock/data";
import { t as pick } from "@vmall/shared";
const props = withDefaults(defineProps<{ pinned?: boolean }>(), { pinned: false });
const { locale } = useI18n();
const open = ref(false);
const hoverId = ref<string | null>(null);
@@ -19,7 +21,7 @@ defineExpose({ open });
</script>
<template>
<div class="catmenu" @mouseleave="hoverId = null">
<div class="catmenu" :class="{ pinned: props.pinned }" @mouseleave="hoverId = null">
<ul class="lvl1">
<li
v-for="g in cats"
@@ -62,6 +64,15 @@ defineExpose({ open });
display: flex;
width: 960px;
}
/* pinned: in-flow left rail of the home hero row, flyout overlays the carousel */
.catmenu.pinned {
position: relative;
top: auto;
left: auto;
flex: none;
width: 240px;
height: 450px;
}
.lvl1 {
width: 240px;
margin: 0;
@@ -131,4 +142,20 @@ defineExpose({ open });
.sub-items a:hover {
color: var(--mall-red);
}
/* pinned overrides: 240x450 rail, flyout opens to the right over the carousel */
.catmenu.pinned .lvl1 {
box-sizing: border-box;
height: 450px;
min-height: 450px;
}
.catmenu.pinned .sub {
position: absolute;
top: 0;
left: 240px;
box-sizing: border-box;
width: 700px;
height: 450px;
min-height: 450px;
overflow: auto;
}
</style>
+4 -9
View File
@@ -32,19 +32,14 @@ onMounted(async () => {
const menuOpen = ref(false);
const isHome = computed(() => route.path === "/");
const collapsed = ref(false);
onMounted(() => {
const onScroll = (): void => {
collapsed.value = window.scrollY > 200;
};
window.addEventListener("scroll", onScroll, { passive: true });
});
const showMenu = computed(() => menuOpen.value || (isHome.value && !collapsed.value));
// The home page pins the category menu in the hero row instead, so the header
// dropdown is hover-only and only on non-home pages.
const showMenu = computed(() => menuOpen.value && !isHome.value);
</script>
<template>
<header class="hdr">
<div v-show="!collapsed" class="w1200 hdr-top">
<div class="w1200 hdr-top">
<NuxtLink to="/" class="logo">VMall</NuxtLink>
<div class="search-box">
<input
+11 -1
View File
@@ -9,7 +9,10 @@ const floors = computed(() => homeFloors().filter((f) => f.products.length > 0))
<template>
<div>
<div class="w1200 hero">
<UiCarousel :images="MOCK_BANNERS" :height="450" />
<ShellCategoryMenu pinned />
<div class="hero-slider">
<UiCarousel :images="MOCK_BANNERS" :height="450" />
</div>
</div>
<div class="w1200 strip">
@@ -49,7 +52,14 @@ const floors = computed(() => homeFloors().filter((f) => f.products.length > 0))
<style scoped>
.hero {
display: flex;
height: 450px;
margin-top: 0;
background: #fff;
}
.hero-slider {
flex: 1;
min-width: 0;
}
.strip {
display: flex;