Unified · Split
| @@ -268,18 +268,156 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo"; · 267 unmodified lines | |||
| 268 | <p | 268 | <p |
| 269 | class="text-[14.5px] leading-[1.55] text-ink-2 mb-6 text-pretty flex-1" | 269 | class="text-[14.5px] leading-[1.55] text-ink-2 mb-6 text-pretty flex-1" |
| 270 | > | 270 | > |
| 271 | We're looking for early adopters to help us develop the | 271 | We're looking for early adopters to give feedback and use the |
| 272 | self-hosted product. If you're a company interested in self | 272 | self hosted product. If you're a company interested in self |
| 273 | hosting get in touch. | 273 | hosting get in touch. |
| 274 | </p> | 274 | </p> |
| 275 | <a href="mailto:rhys@executor.sh" class="btn-link self-start" | 275 | <button |
| 276 | >Get in touch →</a | 276 | type="button" |
| 277 | class="btn-link self-start" | ||
| 278 | data-open-modal="self-host-contact" | ||
| 279 | >Get in touch →</button | ||
| 277 | > | 280 | > |
| 278 | </div> | 281 | </div> |
| 279 | </div> | 282 | </div> |
| 280 | </div> | 283 | </div> |
| 281 | </section> | 284 | </section> |
| 282 | 285 | ||
| 286 | {/* ─── SELF-HOST CONTACT MODAL ─── */} | ||
| 287 | <dialog | ||
| 288 | id="self-host-contact" | ||
| 289 | class="self-host-modal" | ||
| 290 | aria-labelledby="self-host-contact-title" | ||
| 291 | > | ||
| 292 | <div class="surface-card p-7 max-w-[520px] w-full"> | ||
| 293 | <div class="flex items-start justify-between gap-4 mb-5"> | ||
| 294 | <h3 | ||
| 295 | id="self-host-contact-title" | ||
| 296 | class="text-[20px] font-semibold tracking-[-0.01em] text-ink" | ||
| 297 | > | ||
| 298 | Get in touch | ||
| 299 | </h3> | ||
| 300 | <button | ||
| 301 | type="button" | ||
| 302 | class="text-ink-3 hover:text-ink transition-colors" | ||
| 303 | data-close-modal | ||
| 304 | aria-label="Close" | ||
| 305 | > | ||
| 306 | <svg | ||
| 307 | width="18" | ||
| 308 | height="18" | ||
| 309 | viewBox="0 0 24 24" | ||
| 310 | fill="none" | ||
| 311 | stroke="currentColor" | ||
| 312 | stroke-width="2" | ||
| 313 | stroke-linecap="round" | ||
| 314 | stroke-linejoin="round" | ||
| 315 | ><line x1="18" y1="6" x2="6" y2="18"></line><line | ||
| 316 | x1="6" | ||
| 317 | y1="6" | ||
| 318 | x2="18" | ||
| 319 | y2="18"></line></svg | ||
| 320 | > | ||
| 321 | </button> | ||
| 322 | </div> | ||
| 323 | |||
| 324 | <p class="text-[13.5px] leading-[1.55] text-ink-2 mb-2"> | ||
| 325 | Copy the template, fill in your company, and send it over. | ||
| 326 | </p> | ||
| 327 | |||
| 328 | <div class="relative mb-5"> | ||
| 329 | <textarea | ||
| 330 | id="self-host-template" | ||
| 331 | class="w-full text-[14px] leading-[1.55] text-ink bg-surface-2 border border-rule rounded-md p-3 pr-12 font-mono resize-none" | ||
| 332 | rows="3" | ||
| 333 | readonly | ||
| 334 | >Hi Rhys, I'm at {company} and interested in self hosting executor.</textarea | ||
| 335 | > | ||
| 336 | <button | ||
| 337 | type="button" | ||
| 338 | class="copy-btn absolute top-2 right-2 p-1.5 rounded text-ink-3 hover:text-ink hover:bg-surface transition-colors" | ||
| 339 | data-copy-target="self-host-template" | ||
| 340 | aria-label="Copy template to clipboard" | ||
| 341 | > | ||
| 342 | <span class="copy-icons" aria-hidden="true"> | ||
| 343 | <svg | ||
| 344 | class="copy-icon-copy" | ||
| 345 | width="14" | ||
| 346 | height="14" | ||
| 347 | viewBox="0 0 24 24" | ||
| 348 | fill="none" | ||
| 349 | stroke="currentColor" | ||
| 350 | stroke-width="2" | ||
| 351 | stroke-linecap="round" | ||
| 352 | stroke-linejoin="round" | ||
| 353 | ><rect x="9" y="9" width="13" height="13" rx="2" ry="2" | ||
| 354 | ></rect><path | ||
| 355 | d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" | ||
| 356 | ></path></svg | ||
| 357 | > | ||
| 358 | <svg | ||
| 359 | class="copy-icon-check" | ||
| 360 | width="14" | ||
| 361 | height="14" | ||
| 362 | viewBox="0 0 24 24" | ||
| 363 | fill="none" | ||
| 364 | stroke="currentColor" | ||
| 365 | stroke-width="2.5" | ||
| 366 | stroke-linecap="round" | ||
| 367 | stroke-linejoin="round" | ||
| 368 | ><polyline points="20 6 9 17 4 12"></polyline></svg | ||
| 369 | > | ||
| 370 | </span> | ||
| 371 | </button> | ||
| 372 | </div> | ||
| 373 | |||
| 374 | <div | ||
| 375 | class="flex items-center justify-between gap-3 bg-surface-2 border border-rule rounded-md px-3 py-2" | ||
| 376 | > | ||
| 377 | <span class="font-mono text-[14px] text-ink truncate" | ||
| 378 | >rhys@executor.sh</span | ||
| 379 | > | ||
| 380 | <button | ||
| 381 | type="button" | ||
| 382 | class="copy-btn flex items-center gap-1.5 text-[13px] text-ink-2 hover:text-ink transition-colors" | ||
| 383 | data-copy="rhys@executor.sh" | ||
| 384 | aria-label="Copy email to clipboard" | ||
| 385 | > | ||
| 386 | <span class="copy-label">Copy</span> | ||
| 387 | <span class="copy-icons" aria-hidden="true"> | ||
| 388 | <svg | ||
| 389 | class="copy-icon-copy" | ||
| 390 | width="14" | ||
| 391 | height="14" | ||
| 392 | viewBox="0 0 24 24" | ||
| 393 | fill="none" | ||
| 394 | stroke="currentColor" | ||
| 395 | stroke-width="2" | ||
| 396 | stroke-linecap="round" | ||
| 397 | stroke-linejoin="round" | ||
| 398 | ><rect x="9" y="9" width="13" height="13" rx="2" ry="2" | ||
| 399 | ></rect><path | ||
| 400 | d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" | ||
| 401 | ></path></svg | ||
| 402 | > | ||
| 403 | <svg | ||
| 404 | class="copy-icon-check" | ||
| 405 | width="14" | ||
| 406 | height="14" | ||
| 407 | viewBox="0 0 24 24" | ||
| 408 | fill="none" | ||
| 409 | stroke="currentColor" | ||
| 410 | stroke-width="2.5" | ||
| 411 | stroke-linecap="round" | ||
| 412 | stroke-linejoin="round" | ||
| 413 | ><polyline points="20 6 9 17 4 12"></polyline></svg | ||
| 414 | > | ||
| 415 | </span> | ||
| 416 | </button> | ||
| 417 | </div> | ||
| 418 | </div> | ||
| 419 | </dialog> | ||
| 420 | |||
| 283 | {/* ─── FOOTER ─── */} | 421 | {/* ─── FOOTER ─── */} |
| 284 | <footer class="border-t border-rule py-10"> | 422 | <footer class="border-t border-rule py-10"> |
| 285 | <div | 423 | <div |
| @@ -309,23 +447,91 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo"; · 23 unmodified lines | |||
| 309 | </Layout> | 447 | </Layout> |
| 310 | 448 | ||
| 311 | <script> | 449 | <script> |
| 450 | const flashCopied = (() => { | ||
| 451 | const timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>(); | ||
| 452 | return (btn: HTMLElement, ok: boolean) => { | ||
| 453 | btn.dataset.copied = ok ? "true" : "false"; | ||
| 454 | const existing = timers.get(btn); | ||
| 455 | if (existing) clearTimeout(existing); | ||
| 456 | timers.set( | ||
| 457 | btn, | ||
| 458 | setTimeout(() => { | ||
| 459 | delete btn.dataset.copied; | ||
| 460 | timers.delete(btn); | ||
| 461 | }, 1500), | ||
| 462 | ); | ||
| 463 | }; | ||
| 464 | })(); | ||
| 465 | |||
| 312 | document | 466 | document |
| 313 | .querySelectorAll<HTMLButtonElement>("button[data-copy]") | 467 | .querySelectorAll<HTMLButtonElement>("button[data-copy]") |
| 314 | .forEach((btn) => { | 468 | .forEach((btn) => { |
| 315 | let timer: ReturnType<typeof setTimeout> | null = null; | ||
| 316 | |||
| 317 | btn.addEventListener("click", async () => { | 469 | btn.addEventListener("click", async () => { |
| 318 | const text = btn.dataset.copy ?? ""; | 470 | const text = btn.dataset.copy ?? ""; |
| 319 | try { | 471 | try { |
| 320 | await navigator.clipboard.writeText(text); | 472 | await navigator.clipboard.writeText(text); |
| 321 | btn.dataset.copied = "true"; | 473 | flashCopied(btn, true); |
| 322 | } catch { | 474 | } catch { |
| 323 | btn.dataset.copied = "false"; | 475 | flashCopied(btn, false); |
| 324 | } | 476 | } |
| 325 | if (timer) clearTimeout(timer); | 477 | }); |
| 326 | timer = setTimeout(() => { | 478 | }); |
| 327 | delete btn.dataset.copied; | 479 | |
| 328 | }, 1500); | 480 | document |
| 481 | .querySelectorAll<HTMLButtonElement>("button[data-copy-target]") | ||
| 482 | .forEach((btn) => { | ||
| 483 | btn.addEventListener("click", async () => { | ||
| 484 | const id = btn.dataset.copyTarget ?? ""; | ||
| 485 | const target = document.getElementById(id) as | ||
| 486 | | HTMLTextAreaElement | ||
| 487 | | HTMLInputElement | ||
| 488 | | null; | ||
| 489 | const text = target?.value ?? ""; | ||
| 490 | try { | ||
| 491 | await navigator.clipboard.writeText(text); | ||
| 492 | flashCopied(btn, true); | ||
| 493 | } catch { | ||
| 494 | flashCopied(btn, false); | ||
| 495 | } | ||
| 496 | }); | ||
| 497 | }); | ||
| 498 | |||
| 499 | document | ||
| 500 | .querySelectorAll<HTMLButtonElement>("button[data-open-modal]") | ||
| 501 | .forEach((btn) => { | ||
| 502 | btn.addEventListener("click", () => { | ||
| 503 | const id = btn.dataset.openModal ?? ""; | ||
| 504 | const dialog = document.getElementById(id) as HTMLDialogElement | null; | ||
| 505 | dialog?.showModal(); | ||
| 506 | }); | ||
| 507 | }); | ||
| 508 | |||
| 509 | document | ||
| 510 | .querySelectorAll<HTMLDialogElement>("dialog.self-host-modal") | ||
| 511 | .forEach((dialog) => { | ||
| 512 | dialog | ||
| 513 | .querySelectorAll<HTMLButtonElement>("button[data-close-modal]") | ||
| 514 | .forEach((btn) => { | ||
| 515 | btn.addEventListener("click", () => dialog.close()); | ||
| 516 | }); | ||
| 517 | |||
| 518 | dialog.addEventListener("click", (event) => { | ||
| 519 | if (event.target === dialog) dialog.close(); | ||
| 329 | }); | 520 | }); |
| 330 | }); | 521 | }); |
| 331 | </script> | 522 | </script> |
| 523 | |||
| 524 | <style> | ||
| 525 | dialog.self-host-modal { | ||
| 526 | background: transparent; | ||
| 527 | border: none; | ||
| 528 | padding: 0; | ||
| 529 | max-width: 92vw; | ||
| 530 | width: 520px; | ||
| 531 | color: inherit; | ||
| 532 | } | ||
| 533 | dialog.self-host-modal::backdrop { | ||
| 534 | background: rgba(0, 0, 0, 0.5); | ||
| 535 | backdrop-filter: blur(4px); | ||
| 536 | } | ||
| 537 | </style> | ||