- Add tailwindcss v4 + @tailwindcss/vite to mall, shop-admin, admin - Add @vmall/shared/theme.css tokens with html[data-accent] presets - Add @vmall/ui kit (VBtn/VBadge/VField/VInput/VCard/VPanel/VTable/VPage, VAccentSwatch, useAccent) as a Nuxt module - Convert all three apps to kit + utilities; delete ui.css/mall.css and every <style scoped>; consoles get accent presets, mall locked to red - Fix VCard boolean prop default (padding) and PDP/store stale useAsyncData keys on param navigation - Archive adopt-tailwind-design-system; new frontend-ui capability spec
100 lines
3.5 KiB
Vue
100 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
import type { Category } from "@vmall/shared";
|
|
import { t as pick } from "@vmall/shared";
|
|
|
|
const props = withDefaults(defineProps<{ pinned?: boolean }>(), { pinned: false });
|
|
|
|
const { locale } = useI18n();
|
|
const { $api } = useNuxtApp();
|
|
const open = ref(false);
|
|
const hoverId = ref<string | null>(null);
|
|
|
|
// The catalog domain serves this live in Wave 1; the fixed-data adapter answers
|
|
// the same call when it is rolled back. Errors are captured rather than thrown,
|
|
// so a backend outage leaves the menu empty instead of breaking the shell.
|
|
const { data: categories } = await useAsyncData("shell-categories", () => $api.listCategories());
|
|
|
|
const childrenOf = (parentId: string): Category[] =>
|
|
(categories.value ?? [])
|
|
.filter((category) => category.parent_id === parentId)
|
|
.sort((a, b) => a.position - b.position);
|
|
|
|
const cats = computed(() =>
|
|
(categories.value ?? [])
|
|
.filter((category) => category.parent_id === null)
|
|
.sort((a, b) => a.position - b.position)
|
|
.map((cat) => ({
|
|
cat,
|
|
children: childrenOf(cat.id).map((cc) => ({ cat: cc, grandchildren: childrenOf(cc.id) })),
|
|
})),
|
|
);
|
|
|
|
const active = computed(() => cats.value.find((g) => g.cat.id === hoverId.value) ?? null);
|
|
|
|
const rootClass = computed(() =>
|
|
props.pinned
|
|
? "relative z-[700] flex h-hero w-[240px]"
|
|
: "absolute left-0 top-10 z-[700] flex w-[960px]",
|
|
);
|
|
|
|
const railClass = computed(() =>
|
|
props.pinned
|
|
? "box-border h-hero min-h-[450px] w-[240px] list-none bg-[rgba(30,30,30,0.92)] py-2"
|
|
: "min-h-[434px] w-[240px] list-none bg-[rgba(30,30,30,0.92)] py-2",
|
|
);
|
|
|
|
const subClass = computed(() =>
|
|
props.pinned
|
|
? "absolute left-[240px] top-0 box-border h-hero min-h-[450px] w-[700px] overflow-auto bg-surface px-[30px] py-5 shadow-lg"
|
|
: "min-h-[434px] flex-1 bg-surface px-[30px] py-5 shadow-lg",
|
|
);
|
|
|
|
defineExpose({ open });
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="rootClass" @mouseleave="hoverId = null">
|
|
<ul :class="railClass">
|
|
<li
|
|
v-for="g in cats"
|
|
:key="g.cat.id"
|
|
class="cursor-pointer px-[15px] py-[10px]"
|
|
:class="hoverId === g.cat.id ? 'bg-surface' : ''"
|
|
@mouseenter="hoverId = g.cat.id"
|
|
>
|
|
<NuxtLink
|
|
:to="`/search?category=${g.cat.id}`"
|
|
class="block text-sm no-underline"
|
|
:class="hoverId === g.cat.id ? 'text-primary' : 'text-white'"
|
|
>{{ pick(g.cat.name, locale) }}</NuxtLink>
|
|
<span class="mt-0.5 block">
|
|
<NuxtLink
|
|
v-for="cc in g.children.slice(0, 3)"
|
|
:key="cc.cat.id"
|
|
:to="`/search?category=${cc.cat.id}`"
|
|
class="mr-2 text-[12px] no-underline"
|
|
:class="hoverId === g.cat.id ? 'text-primary' : 'text-[#bbb] hover:text-white'"
|
|
>{{ pick(cc.cat.name, locale) }}</NuxtLink>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
|
|
<div v-if="active" :class="subClass">
|
|
<div v-for="cc in active.children" :key="cc.cat.id" class="flex border-b border-border py-3 last:border-b-0">
|
|
<NuxtLink
|
|
:to="`/search?category=${cc.cat.id}`"
|
|
class="w-[120px] shrink-0 text-[14px] font-semibold text-text no-underline"
|
|
>{{ pick(cc.cat.name, locale) }}</NuxtLink>
|
|
<div>
|
|
<NuxtLink
|
|
v-for="gc in cc.grandchildren"
|
|
:key="gc.id"
|
|
:to="`/search?category=${gc.id}`"
|
|
class="mb-2 mr-[18px] inline-block text-[13px] text-muted no-underline transition-colors hover:text-primary"
|
|
>{{ pick(gc.name, locale) }}</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|