Newer
Older
gnexus-ui-kit / src / vue / components / GnRouterTabs.js
/**
 * GnRouterTabs — Router-aware tab switcher.
 *
 * Wraps GnTabs and drives active state from the current vue-router route.
 * When a tab is activated, the component calls router.push(item.to)
 * instead of emitting update:modelValue.
 *
 * @typedef {Object} GnRouterTabsItem
 * @property {string} id - Slot name and tab identifier
 * @property {string|Object} to - Route target (string path or { name, params, query })
 * @property {string} label - Tab label text
 * @property {string} [icon] - Phosphor icon name with ph- prefix
 * @property {boolean} [disabled] - Disabled state
 *
 * @typedef {Object} GnRouterTabsProps
 * @property {Array} items - Array of GnRouterTabsItem
 * @property {boolean} [compact=false] - Compact size
 * @property {boolean} [vertical=false] - Vertical layout
 * @property {string} [ariaLabel='Tabs'] - ARIA label
 * @property {string} [activeMatch='prefix'] - 'exact' | 'prefix' — how to match current route against item.to
 *
 * @slots [item.id] - One slot per item id
 */
import { computed, defineComponent, h, watch } from "vue";
import { tryUseRouter, tryUseRoute, isRouteActive } from "../composables/useVueRouter.js";
import GnTabs from "./GnTabs.js";

export default defineComponent({
	name: "GnRouterTabs",
	props: {
		items: { type: Array, required: true },
		compact: { type: Boolean, default: false },
		vertical: { type: Boolean, default: false },
		ariaLabel: { type: String, default: "Tabs" },
		activeMatch: { type: String, default: "prefix" }
	},
	setup(props, { slots }) {
		const router = tryUseRouter();
		const route = tryUseRoute();

		const hasRouter = Boolean(router && route);

		if(!hasRouter && typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production") {
			// eslint-disable-next-line no-console
			console.warn("[gnexus-ui-kit] GnRouterTabs requires vue-router. Falling back to standard tabs.");
		}

		const tabItems = computed(() => props.items.map(item => ({
			id: item.id,
			label: item.label,
			icon: item.icon,
			disabled: item.disabled,
			to: item.to
		})));

		const activeId = computed(() => {
			if(!hasRouter) {
				return tabItems.value.find(item => !item.disabled)?.id || "";
			}

			const matched = tabItems.value.find(item => {
				if(item.disabled || !item.to) {
					return false;
				}

				return isRouteActive(route, item.to, props.activeMatch);
			});

			return matched?.id || tabItems.value.find(item => !item.disabled)?.id || "";
		});

		const onUpdate = id => {
			if(!hasRouter) {
				return;
			}

			const item = tabItems.value.find(i => i.id === id);

			if(item && item.to) {
				router.push(item.to);
			}
		};

		return () => h(GnTabs, {
			modelValue: activeId.value,
			items: tabItems.value,
			compact: props.compact,
			vertical: props.vertical,
			ariaLabel: props.ariaLabel,
			"onUpdate:modelValue": onUpdate
		}, slots);
	}
});