| import logoLight from "../asset/logo-ornate-light.svg" |
| import logoDark from "../asset/logo-ornate-dark.svg" |
| import copyLogoLight from "../asset/lander/logo-light.svg" |
| import copyLogoDark from "../asset/lander/logo-dark.svg" |
| import copyWordmarkLight from "../asset/lander/wordmark-light.svg" |
| import copyWordmarkDark from "../asset/lander/wordmark-dark.svg" |
| import copyBrandAssetsLight from "../asset/lander/brand-assets-light.svg" |
| import copyBrandAssetsDark from "../asset/lander/brand-assets-dark.svg" |
|
|
| |
| |
| import copyLogoSvgLight from "../asset/lander/opencode-logo-light.svg" |
| import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg" |
| import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg" |
| import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg" |
| import { A, useNavigate } from "@solidjs/router" |
| import { createMemo, Match, Show, Switch } from "solid-js" |
| import { createStore } from "solid-js/store" |
| import { createEffect, onCleanup } from "solid-js" |
| import { config } from "~/config" |
| import { useI18n } from "~/context/i18n" |
| import { useLanguage } from "~/context/language" |
| import "./header-context-menu.css" |
|
|
| const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches |
|
|
| const fetchSvgContent = async (svgPath: string): Promise<string> => { |
| try { |
| const response = await fetch(svgPath) |
| const svgText = await response.text() |
| return svgText |
| } catch (err) { |
| console.error("Failed to fetch SVG content:", err) |
| throw err |
| } |
| } |
|
|
| export function Header(props: { zen?: boolean; go?: boolean; hideGetStarted?: boolean }) { |
| const navigate = useNavigate() |
| const i18n = useI18n() |
| const language = useLanguage() |
|
|
| const [store, setStore] = createStore({ |
| mobileMenuOpen: false, |
| contextMenuOpen: false, |
| contextMenuPosition: { x: 0, y: 0 }, |
| }) |
|
|
| createEffect(() => { |
| const handleClickOutside = () => { |
| setStore("contextMenuOpen", false) |
| } |
|
|
| const handleContextMenu = (event: MouseEvent) => { |
| event.preventDefault() |
| setStore("contextMenuOpen", false) |
| } |
|
|
| const handleKeyDown = (event: KeyboardEvent) => { |
| if (event.key === "Escape") { |
| setStore("contextMenuOpen", false) |
| } |
| } |
|
|
| if (store.contextMenuOpen) { |
| document.addEventListener("click", handleClickOutside) |
| document.addEventListener("contextmenu", handleContextMenu) |
| document.addEventListener("keydown", handleKeyDown) |
| onCleanup(() => { |
| document.removeEventListener("click", handleClickOutside) |
| document.removeEventListener("contextmenu", handleContextMenu) |
| document.removeEventListener("keydown", handleKeyDown) |
| }) |
| } |
| }) |
|
|
| const handleLogoContextMenu = (event: MouseEvent) => { |
| event.preventDefault() |
| const logoElement = (event.currentTarget as HTMLElement).querySelector("a") |
| if (logoElement) { |
| const rect = logoElement.getBoundingClientRect() |
| setStore("contextMenuPosition", { |
| x: rect.left - 16, |
| y: rect.bottom + 8, |
| }) |
| } |
| setStore("contextMenuOpen", true) |
| } |
|
|
| const copyWordmarkToClipboard = async () => { |
| try { |
| const isDark = isDarkMode() |
| const wordmarkSvgPath = isDark ? copyWordmarkSvgDark : copyWordmarkSvgLight |
| const wordmarkSvg = await fetchSvgContent(wordmarkSvgPath) |
| await navigator.clipboard.writeText(wordmarkSvg) |
| } catch (err) { |
| console.error("Failed to copy wordmark to clipboard:", err) |
| } |
| } |
|
|
| const copyLogoToClipboard = async () => { |
| try { |
| const isDark = isDarkMode() |
| const logoSvgPath = isDark ? copyLogoSvgDark : copyLogoSvgLight |
| const logoSvg = await fetchSvgContent(logoSvgPath) |
| await navigator.clipboard.writeText(logoSvg) |
| } catch (err) { |
| console.error("Failed to copy logo to clipboard:", err) |
| } |
| } |
|
|
| return ( |
| <section data-component="top"> |
| <div onContextMenu={handleLogoContextMenu}> |
| <A href={language.route("/")}> |
| <img data-slot="logo light" src={logoLight} alt={i18n.t("nav.logoAlt")} width="189" height="34" /> |
| <img data-slot="logo dark" src={logoDark} alt={i18n.t("nav.logoAlt")} width="189" height="34" /> |
| </A> |
| </div> |
| |
| <Show when={store.contextMenuOpen}> |
| <div |
| class="context-menu" |
| style={`left: ${store.contextMenuPosition.x}px; top: ${store.contextMenuPosition.y}px;`} |
| > |
| <button class="context-menu-item" onClick={copyLogoToClipboard}> |
| <img data-slot="copy light" src={copyLogoLight} alt="" /> |
| <img data-slot="copy dark" src={copyLogoDark} alt="" /> |
| {i18n.t("nav.context.copyLogo")} |
| </button> |
| <button class="context-menu-item" onClick={copyWordmarkToClipboard}> |
| <img data-slot="copy light" src={copyWordmarkLight} alt="" /> |
| <img data-slot="copy dark" src={copyWordmarkDark} alt="" /> |
| {i18n.t("nav.context.copyWordmark")} |
| </button> |
| <button class="context-menu-item" onClick={() => navigate(language.route("/brand"))}> |
| <img data-slot="copy light" src={copyBrandAssetsLight} alt="" /> |
| <img data-slot="copy dark" src={copyBrandAssetsDark} alt="" /> |
| {i18n.t("nav.context.brandAssets")} |
| </button> |
| </div> |
| </Show> |
| <nav data-component="nav-desktop"> |
| <ul> |
| <li> |
| <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;"> |
| {i18n.t("nav.github")} |
| </a> |
| </li> |
| <li> |
| <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a> |
| </li> |
| <li> |
| <a href={language.route("/data")}>{i18n.t("nav.data")}</a> |
| </li> |
| <li> |
| <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A> |
| </li> |
| <li> |
| <A href={language.route("/go")}>{i18n.t("nav.go")}</A> |
| </li> |
| <li> |
| <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A> |
| </li> |
| <Show when={props.zen || props.go}> |
| <li> |
| <a href="/auth">{i18n.t("nav.login")}</a> |
| </li> |
| </Show> |
| <Show when={!props.hideGetStarted}> |
| <li> |
| <A href={language.route("/download")} data-slot="cta-button"> |
| <svg |
| width="18" |
| height="18" |
| viewBox="0 0 18 18" |
| fill="none" |
| xmlns="http://www.w3.org/2000/svg" |
| style="flex-shrink: 0;" |
| > |
| <path |
| d="M12.1875 9.75L9.00001 12.9375L5.8125 9.75M9.00001 2.0625L9 12.375M14.4375 15.9375H3.5625" |
| stroke="currentColor" |
| stroke-width="1.5" |
| stroke-linecap="square" |
| /> |
| </svg> |
| {i18n.t("nav.free")} |
| </A> |
| </li> |
| </Show> |
| </ul> |
| </nav> |
| <nav data-component="nav-mobile"> |
| <button |
| type="button" |
| data-component="nav-mobile-toggle" |
| aria-expanded="false" |
| aria-controls="nav-mobile-menu" |
| class="nav-toggle" |
| onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)} |
| > |
| <span class="sr-only">{i18n.t("nav.openMenu")}</span> |
| <Switch> |
| <Match when={store.mobileMenuOpen}> |
| <svg |
| class="icon icon-close" |
| width="24" |
| height="24" |
| viewBox="0 0 24 24" |
| fill="none" |
| aria-hidden="true" |
| xmlns="http://www.w3.org/2000/svg" |
| > |
| <path |
| d="M12.7071 11.9993L18.0104 17.3026L17.3033 18.0097L12 12.7064L6.6967 18.0097L5.98959 17.3026L11.2929 11.9993L5.98959 6.69595L6.6967 5.98885L12 11.2921L17.3033 5.98885L18.0104 6.69595L12.7071 11.9993Z" |
| fill="currentColor" |
| /> |
| </svg> |
| </Match> |
| <Match when={!store.mobileMenuOpen}> |
| <svg |
| class="icon icon-hamburger" |
| width="24" |
| height="24" |
| viewBox="0 0 24 24" |
| fill="none" |
| aria-hidden="true" |
| xmlns="http://www.w3.org/2000/svg" |
| > |
| <path d="M19 17H5V16H19V17Z" fill="currentColor" /> |
| <path d="M19 8H5V7H19V8Z" fill="currentColor" /> |
| </svg> |
| </Match> |
| </Switch> |
| </button> |
| |
| <Show when={store.mobileMenuOpen}> |
| <div id="nav-mobile-menu" data-component="nav-mobile"> |
| <nav data-component="nav-mobile-menu-list"> |
| <ul> |
| <li> |
| <A href={language.route("/")}>{i18n.t("nav.home")}</A> |
| </li> |
| <li> |
| <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;"> |
| {i18n.t("nav.github")} |
| </a> |
| </li> |
| <li> |
| <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a> |
| </li> |
| <li> |
| <a href={language.route("/data")}>{i18n.t("nav.data")}</a> |
| </li> |
| <Show when={!props.zen}> |
| <li> |
| <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A> |
| </li> |
| </Show> |
| <Show when={!props.go}> |
| <li> |
| <A href={language.route("/go")}>{i18n.t("nav.go")}</A> |
| </li> |
| </Show> |
| <li> |
| <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A> |
| </li> |
| <Show when={props.zen || props.go}> |
| <li> |
| <a href="/auth">{i18n.t("nav.login")}</a> |
| </li> |
| </Show> |
| <Show when={!props.hideGetStarted}> |
| <li> |
| <A href={language.route("/download")} data-slot="cta-button"> |
| {i18n.t("nav.getStartedFree")} |
| </A> |
| </li> |
| </Show> |
| </ul> |
| </nav> |
| </div> |
| </Show> |
| </nav> |
| </section> |
| ) |
| } |
|
|