header.tsx 9.7 KB

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