header.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import logoLight from "../asset/logo-ornate-light.svg"
  2. import logoDark from "../asset/logo-ornate-dark.svg"
  3. import copyLogoLight from "../asset/lander/logo-light.svg"
  4. import copyLogoDark from "../asset/lander/logo-dark.svg"
  5. import copyWordmarkLight from "../asset/lander/wordmark-light.svg"
  6. import copyWordmarkDark from "../asset/lander/wordmark-dark.svg"
  7. import copyBrandAssetsLight from "../asset/lander/brand-assets-light.svg"
  8. import copyBrandAssetsDark from "../asset/lander/brand-assets-dark.svg"
  9. // SVG files for copying (separate from button icons)
  10. // Replace these with your actual SVG files for copying
  11. import copyLogoSvgLight from "../asset/lander/opencode-logo-light.svg"
  12. import copyLogoSvgDark from "../asset/lander/opencode-logo-dark.svg"
  13. import copyWordmarkSvgLight from "../asset/lander/opencode-wordmark-light.svg"
  14. import copyWordmarkSvgDark from "../asset/lander/opencode-wordmark-dark.svg"
  15. import { A, createAsync, useNavigate } from "@solidjs/router"
  16. import { createMemo, Match, Show, Switch } from "solid-js"
  17. import { createStore } from "solid-js/store"
  18. import { github } from "~/lib/github"
  19. import { createEffect, onCleanup } from "solid-js"
  20. import { config } from "~/config"
  21. import { useI18n } from "~/context/i18n"
  22. import { useLanguage } from "~/context/language"
  23. import "./header-context-menu.css"
  24. const isDarkMode = () => window.matchMedia("(prefers-color-scheme: dark)").matches
  25. const fetchSvgContent = async (svgPath: string): Promise<string> => {
  26. try {
  27. const response = await fetch(svgPath)
  28. const svgText = await response.text()
  29. return svgText
  30. } catch (err) {
  31. console.error("Failed to fetch SVG content:", err)
  32. throw err
  33. }
  34. }
  35. export function Header(props: { zen?: boolean; hideGetStarted?: boolean }) {
  36. const navigate = useNavigate()
  37. const i18n = useI18n()
  38. const language = useLanguage()
  39. const githubData = createAsync(() => github())
  40. const starCount = createMemo(() =>
  41. githubData()?.stars
  42. ? new Intl.NumberFormat("en-US", {
  43. notation: "compact",
  44. compactDisplay: "short",
  45. maximumFractionDigits: 0,
  46. }).format(githubData()?.stars!)
  47. : config.github.starsFormatted.compact,
  48. )
  49. const [store, setStore] = createStore({
  50. mobileMenuOpen: false,
  51. contextMenuOpen: false,
  52. contextMenuPosition: { x: 0, y: 0 },
  53. })
  54. createEffect(() => {
  55. const handleClickOutside = () => {
  56. setStore("contextMenuOpen", false)
  57. }
  58. const handleContextMenu = (event: MouseEvent) => {
  59. event.preventDefault()
  60. setStore("contextMenuOpen", false)
  61. }
  62. const handleKeyDown = (event: KeyboardEvent) => {
  63. if (event.key === "Escape") {
  64. setStore("contextMenuOpen", false)
  65. }
  66. }
  67. if (store.contextMenuOpen) {
  68. document.addEventListener("click", handleClickOutside)
  69. document.addEventListener("contextmenu", handleContextMenu)
  70. document.addEventListener("keydown", handleKeyDown)
  71. onCleanup(() => {
  72. document.removeEventListener("click", handleClickOutside)
  73. document.removeEventListener("contextmenu", handleContextMenu)
  74. document.removeEventListener("keydown", handleKeyDown)
  75. })
  76. }
  77. })
  78. const handleLogoContextMenu = (event: MouseEvent) => {
  79. event.preventDefault()
  80. const logoElement = (event.currentTarget as HTMLElement).querySelector("a")
  81. if (logoElement) {
  82. const rect = logoElement.getBoundingClientRect()
  83. setStore("contextMenuPosition", {
  84. x: rect.left - 16,
  85. y: rect.bottom + 8,
  86. })
  87. }
  88. setStore("contextMenuOpen", true)
  89. }
  90. const copyWordmarkToClipboard = async () => {
  91. try {
  92. const isDark = isDarkMode()
  93. const wordmarkSvgPath = isDark ? copyWordmarkSvgDark : copyWordmarkSvgLight
  94. const wordmarkSvg = await fetchSvgContent(wordmarkSvgPath)
  95. await navigator.clipboard.writeText(wordmarkSvg)
  96. } catch (err) {
  97. console.error("Failed to copy wordmark to clipboard:", err)
  98. }
  99. }
  100. const copyLogoToClipboard = async () => {
  101. try {
  102. const isDark = isDarkMode()
  103. const logoSvgPath = isDark ? copyLogoSvgDark : copyLogoSvgLight
  104. const logoSvg = await fetchSvgContent(logoSvgPath)
  105. await navigator.clipboard.writeText(logoSvg)
  106. } catch (err) {
  107. console.error("Failed to copy logo to clipboard:", err)
  108. }
  109. }
  110. return (
  111. <section data-component="top">
  112. <div onContextMenu={handleLogoContextMenu}>
  113. <A href={language.route("/")}>
  114. <img data-slot="logo light" src={logoLight} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
  115. <img data-slot="logo dark" src={logoDark} alt={i18n.t("nav.logoAlt")} width="189" height="34" />
  116. </A>
  117. </div>
  118. <Show when={store.contextMenuOpen}>
  119. <div
  120. class="context-menu"
  121. style={`left: ${store.contextMenuPosition.x}px; top: ${store.contextMenuPosition.y}px;`}
  122. >
  123. <button class="context-menu-item" onClick={copyLogoToClipboard}>
  124. <img data-slot="copy light" src={copyLogoLight} alt="" />
  125. <img data-slot="copy dark" src={copyLogoDark} alt="" />
  126. {i18n.t("nav.context.copyLogo")}
  127. </button>
  128. <button class="context-menu-item" onClick={copyWordmarkToClipboard}>
  129. <img data-slot="copy light" src={copyWordmarkLight} alt="" />
  130. <img data-slot="copy dark" src={copyWordmarkDark} alt="" />
  131. {i18n.t("nav.context.copyWordmark")}
  132. </button>
  133. <button class="context-menu-item" onClick={() => navigate(language.route("/brand"))}>
  134. <img data-slot="copy light" src={copyBrandAssetsLight} alt="" />
  135. <img data-slot="copy dark" src={copyBrandAssetsDark} alt="" />
  136. {i18n.t("nav.context.brandAssets")}
  137. </button>
  138. </div>
  139. </Show>
  140. <nav data-component="nav-desktop">
  141. <ul>
  142. <li>
  143. <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
  144. {i18n.t("nav.github")} <span>[{starCount()}]</span>
  145. </a>
  146. </li>
  147. <li>
  148. <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
  149. </li>
  150. <Show when={!props.zen}>
  151. <li>
  152. <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
  153. </li>
  154. </Show>
  155. <li>
  156. <A href={language.route("/go")}>{i18n.t("nav.go")}</A>
  157. </li>
  158. <li>
  159. <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
  160. </li>
  161. <Show when={props.zen}>
  162. <li>
  163. <a href="/auth">{i18n.t("nav.login")}</a>
  164. </li>
  165. </Show>
  166. <Show when={!props.hideGetStarted}>
  167. <li>
  168. <A href={language.route("/download")} data-slot="cta-button">
  169. <svg
  170. width="18"
  171. height="18"
  172. viewBox="0 0 18 18"
  173. fill="none"
  174. xmlns="http://www.w3.org/2000/svg"
  175. style="flex-shrink: 0;"
  176. >
  177. <path
  178. d="M12.1875 9.75L9.00001 12.9375L5.8125 9.75M9.00001 2.0625L9 12.375M14.4375 15.9375H3.5625"
  179. stroke="currentColor"
  180. stroke-width="1.5"
  181. stroke-linecap="square"
  182. />
  183. </svg>
  184. {i18n.t("nav.free")}
  185. </A>
  186. </li>
  187. </Show>
  188. </ul>
  189. </nav>
  190. <nav data-component="nav-mobile">
  191. <button
  192. type="button"
  193. data-component="nav-mobile-toggle"
  194. aria-expanded="false"
  195. aria-controls="nav-mobile-menu"
  196. class="nav-toggle"
  197. onClick={() => setStore("mobileMenuOpen", !store.mobileMenuOpen)}
  198. >
  199. <span class="sr-only">{i18n.t("nav.openMenu")}</span>
  200. <Switch>
  201. <Match when={store.mobileMenuOpen}>
  202. <svg
  203. class="icon icon-close"
  204. width="24"
  205. height="24"
  206. viewBox="0 0 24 24"
  207. fill="none"
  208. aria-hidden="true"
  209. xmlns="http://www.w3.org/2000/svg"
  210. >
  211. <path
  212. 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"
  213. fill="currentColor"
  214. />
  215. </svg>
  216. </Match>
  217. <Match when={!store.mobileMenuOpen}>
  218. <svg
  219. class="icon icon-hamburger"
  220. width="24"
  221. height="24"
  222. viewBox="0 0 24 24"
  223. fill="none"
  224. aria-hidden="true"
  225. xmlns="http://www.w3.org/2000/svg"
  226. >
  227. <path d="M19 17H5V16H19V17Z" fill="currentColor" />
  228. <path d="M19 8H5V7H19V8Z" fill="currentColor" />
  229. </svg>
  230. </Match>
  231. </Switch>
  232. </button>
  233. <Show when={store.mobileMenuOpen}>
  234. <div id="nav-mobile-menu" data-component="nav-mobile">
  235. <nav data-component="nav-mobile-menu-list">
  236. <ul>
  237. <li>
  238. <A href={language.route("/")}>{i18n.t("nav.home")}</A>
  239. </li>
  240. <li>
  241. <a href={config.github.repoUrl} target="_blank" style="white-space: nowrap;">
  242. {i18n.t("nav.github")} <span>[{starCount()}]</span>
  243. </a>
  244. </li>
  245. <li>
  246. <a href={language.route("/docs")}>{i18n.t("nav.docs")}</a>
  247. </li>
  248. <Show when={!props.zen}>
  249. <li>
  250. <A href={language.route("/zen")}>{i18n.t("nav.zen")}</A>
  251. </li>
  252. </Show>
  253. <li>
  254. <A href={language.route("/go")}>{i18n.t("nav.go")}</A>
  255. </li>
  256. <li>
  257. <A href={language.route("/enterprise")}>{i18n.t("nav.enterprise")}</A>
  258. </li>
  259. <Show when={props.zen}>
  260. <li>
  261. <a href="/auth">{i18n.t("nav.login")}</a>
  262. </li>
  263. </Show>
  264. <Show when={!props.hideGetStarted}>
  265. <li>
  266. <A href={language.route("/download")} data-slot="cta-button">
  267. {i18n.t("nav.getStartedFree")}
  268. </A>
  269. </li>
  270. </Show>
  271. </ul>
  272. </nav>
  273. </div>
  274. </Show>
  275. </nav>
  276. </section>
  277. )
  278. }