Newer
Older
vmk-ui-kit / src / vue / components / GnTopNavbar.js
@Eugene Sukhodolskiy Eugene Sukhodolskiy 19 hours ago 4 KB so many changes
/**
 * GnTopNavbar — application-level top header bar.
 *
 * Figma: Top Navbar.
 *
 * Variants:
 *   ghost    - transparent over dark surfaces (logo + center nav + pill action).
 *   solid    - white bar with title/actions (back buttons, outline actions).
 *   centered - centered logo/brand only.
 *   compact  - mobile-style bar with back arrow, title/meta, and optional action.
 *
 * @typedef {Object} GnTopNavbarBrand
 * @property {string} title - Brand text.
 * @property {boolean} [logo=true] - Show the default logo orb.
 *
 * @typedef {Object} GnTopNavbarAction
 * @property {string} label - Action text.
 * @property {string} [icon] - VMK icon key or legacy Phosphor name (ph-*).
 * @property {string} [href] - Link URL.
 * @property {string} [variant] - "pill" (white pill) or "outline" (mint outline). Ghost defaults to pill.
 * @property {Function} [onClick] - Click handler.
 *
 * @slots brand - Left brand region.
 * @slots center - Center region (overrides title/navItems).
 * @slots actions - Right actions region (overrides actions prop).
 */

import { defineComponent, h, ref, watch } from "vue";
import { cx, iconNode } from "../utils.js";
import GnBottomBar from "./GnBottomBar.js";

export default defineComponent({
	name: "GnTopNavbar",
	inheritAttrs: false,
	props: {
		variant: { type: String, default: "ghost" },
		brand: { type: [String, Object], default: "" },
		title: { type: String, default: "" },
		subtitle: { type: String, default: "" },
		navItems: { type: Array, default: () => [] },
		actions: { type: Array, default: () => [] },
		back: { type: Boolean, default: false },
		activeNav: { type: [String, Number], default: undefined }
	},
	emits: ["select", "back"],
	setup(props, { attrs, slots, emit }) {
		const centerActive = ref(props.activeNav ?? (props.navItems.length ? 0 : undefined));

		watch(() => props.activeNav, value => {
			if (value !== undefined) {
				centerActive.value = value;
			}
		});

		const brandTitle = typeof props.brand === "string" ? props.brand : props.brand?.title;
		const showLogo = typeof props.brand === "object" ? props.brand?.logo !== false : true;

		const renderBrand = () => {
			if (slots.brand) {
				return slots.brand();
			}

			if (!brandTitle) {
				return null;
			}

			return h("a", { class: "top-navbar__brand", href: "#" }, [
				showLogo && h("span", { class: "top-navbar__logo" }),
				h("span", { class: "top-navbar__title" }, brandTitle)
			]);
		};

		const renderCenterContent = () => {
			if (slots.center) {
				return slots.center();
			}

			if (props.title) {
				return h("h1", { class: "top-navbar__title" }, props.title);
			}

			if (props.navItems.length) {
				return h(GnBottomBar, {
					class: "top-navbar__nav",
					layout: "inline",
					items: props.navItems,
					modelValue: centerActive.value,
					"onUpdate:modelValue": value => {
						centerActive.value = value;
						emit("select", value);
					}
				});
			}

			return null;
		};

		const renderCenter = () => {
			const content = [renderCenterContent()];

			if (props.subtitle) {
				content.push(h("span", { class: "top-navbar__meta" }, props.subtitle));
			}

			const stack = props.subtitle && !slots.center;

			return h("div", { class: cx("top-navbar__center", { "top-navbar__center--stack": stack }) }, content);
		};

		const renderActions = () => {
			if (slots.actions) {
				return slots.actions();
			}

			return props.actions.map((action, index) => {
				const tag = action.href ? "a" : "button";
				const isPill = action.variant === "pill" || (props.variant === "ghost" && action.variant !== "outline");

				return h(tag, {
					key: index,
					class: isPill ? "top-navbar__pill" : cx("btn", "btn-secondary", "btn-sm"),
					href: action.href,
					type: action.href ? undefined : "button",
					onClick: () => action.onClick?.(action)
				}, [
					iconNode(action.icon),
					action.label
				]);
			});
		};

		return () => {
			const children = [];

			if (props.variant === "centered") {
				children.push(h("div", { class: "top-navbar__center" }, slots.center?.() ?? renderBrand()));
			} else {
				children.push(renderBrand());

				if (props.back) {
					children.push(h("button", {
						class: cx("btn", "btn-icon-only", "btn-sm", "btn-secondary", "top-navbar__back"),
						type: "button",
						onClick: () => emit("back")
					}, [iconNode("ph-arrow-left")]));
				}

				children.push(renderCenter());
				children.push(h("div", { class: "top-navbar__actions" }, [renderActions()]));
			}

			return h("header", {
				...attrs,
				class: cx("top-navbar", `top-navbar--${props.variant}`, attrs.class)
			}, children);
		};
	}
});