Unified · Split
| @@ -268,18 +268,156 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo"; · 267 unmodified lines | ||
| 268 | 268 | <p |
| 269 | 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 | |
| 272 | − self-hosted product. If you're a company interested in self | |
| 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 | |
| 273 | 273 | hosting get in touch. |
| 274 | 274 | </p> |
| 275 | − <a href="mailto:rhys@executor.sh" class="btn-link self-start" | |
| 276 | − >Get in touch →</a | |
| 275 | + <button | |
| 276 | + type="button" | |
| 277 | + class="btn-link self-start" | |
| 278 | + data-open-modal="self-host-contact" | |
| 279 | + >Get in touch →</button | |
| 277 | 280 | > |
| 278 | 281 | </div> |
| 279 | 282 | </div> |
| 280 | 283 | </div> |
| 281 | 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 | 421 | {/* ─── FOOTER ─── */} |
| 284 | 422 | <footer class="border-t border-rule py-10"> |
| 285 | 423 | <div |
| @@ -309,23 +447,91 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo"; · 23 unmodified lines | ||
| 309 | 447 | </Layout> |
| 310 | 448 | |
| 311 | 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 | 466 | document |
| 313 | 467 | .querySelectorAll<HTMLButtonElement>("button[data-copy]") |
| 314 | 468 | .forEach((btn) => { |
| 315 | − let timer: ReturnType<typeof setTimeout> | null = null; | |
| 316 | − | |
| 317 | 469 | btn.addEventListener("click", async () => { |
| 318 | 470 | const text = btn.dataset.copy ?? ""; |
| 319 | 471 | try { |
| 320 | 472 | await navigator.clipboard.writeText(text); |
| 321 | − btn.dataset.copied = "true"; | |
| 473 | + flashCopied(btn, true); | |
| 322 | 474 | } catch { |
| 323 | − btn.dataset.copied = "false"; | |
| 475 | + flashCopied(btn, false); | |
| 324 | 476 | } |
| 325 | − if (timer) clearTimeout(timer); | |
| 326 | − timer = setTimeout(() => { | |
| 327 | − delete btn.dataset.copied; | |
| 328 | − }, 1500); | |
| 477 | + }); | |
| 478 | + }); | |
| 479 | + | |
| 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 | 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> | |