/**
* GnBottomBar — floating pill navigation bar.
*
* Figma: Bottom Bar Container.
*
* Supports two layouts:
* stacked - icon above label, all items inside a white pill (mobile nav).
* inline - icon left of label, only the active item is a white pill.
*
* @typedef {Object} GnBottomBarItem
* @property {string} label - Item text.
* @property {string} [icon] - VMK icon key or legacy Phosphor name (ph-*).
* @property {string|Object} [to] - vue-router route.
* @property {string} [href] - Plain link URL.
* @property {string|number} [key] - Stable key used with v-model.
* @property {boolean} [active=false] - Force active state.
* @property {boolean} [disabled=false] - Disable the item.
*
* @slots item - Replace the default label/icon for an item via item slot (scope { item, active }).
*/
import { computed, defineComponent, h } from "vue";
import { cx, iconNode } from "../utils.js";
import { tryUseRouter, tryUseRoute, isRouteActive } from "../composables/useVueRouter.js";
export default defineComponent({
name: "GnBottomBar",
inheritAttrs: false,
props: {
items: { type: Array, default: () => [] },
layout: { type: String, default: "stacked" },
modelValue: { type: [String, Number], default: undefined },
activeMatch: { type: String, default: "prefix" }
},
emits: ["select", "update:modelValue"],
setup(props, { attrs, emit, slots }) {
const router = tryUseRouter();
const route = tryUseRoute();
const hasRouter = Boolean(router && route);
const resolveHref = to => {
if (!to) {
return undefined;
}
if (typeof to === "string") {
return to;
}
if (to.path) {
return to.path;
}
return undefined;
};
const isItemActive = (item, index) => {
if (item.to && hasRouter) {
return isRouteActive(route, item.to, props.activeMatch);
}
if (item.active) {
return true;
}
if (props.modelValue !== undefined) {
return (item.key !== undefined && item.key === props.modelValue) || index === props.modelValue;
}
return false;
};
const navItems = computed(() => props.items.map((item, index) => {
const hasTo = Boolean(item.to);
const resolvedHref = hasTo
? (hasRouter ? router.resolve(item.to).href : resolveHref(item.to))
: item.href;
return {
...item,
resolvedHref,
hasTo,
isActive: isItemActive(item, index)
};
}));
const activate = (item, index, event) => {
if (item.disabled) {
event.preventDefault();
return;
}
if (item.hasTo && hasRouter) {
event.preventDefault();
router.push(item.to);
}
if (props.modelValue !== undefined) {
const nextValue = item.key !== undefined ? item.key : index;
emit("update:modelValue", nextValue);
}
item.onSelect?.(item, event);
emit("select", item);
};
return () => h("nav", {
...attrs,
class: cx("bottom-bar", `bottom-bar--${props.layout}`, attrs.class)
}, navItems.value.map((item, index) => {
const tag = item.resolvedHref ? "a" : "button";
const isActive = item.isActive;
return h(tag, {
key: item.key ?? index,
class: cx("bottom-bar__item", { "is-active": isActive, "is-disabled": item.disabled }),
href: item.resolvedHref,
type: item.resolvedHref ? undefined : "button",
disabled: item.disabled || undefined,
"aria-current": isActive ? "page" : undefined,
onClick: event => activate(item, index, event)
}, [
slots.item?.({ item, active: isActive }) || [
h("span", { class: "bottom-bar__icon" }, [iconNode(item.icon)]),
h("span", { class: "bottom-bar__label" }, item.label)
]
]);
}));
}
});