History

fix(marketing): inline graph paper SVG, swap copy link for icon button

The /pattern-graph-paper.svg public asset was 404ing in production, so the hero grid pattern never rendered. Inline the SVG as a data URI in the mask so it can never miss. While here, turn the "$ npx executor web" hero link into a button that copies the command to the clipboard, with a copy icon that crossfades to a check on success.

Rhys Sullivan committed 6 months ago

Commit 880c1261 parent d1d7c992 files · +60 −4Browse files

Unified · Split

2 files
  1. apps/marketing/src/pages/index.astro +26 −2
  2. apps/marketing/src/styles/global.css +34 −2
apps/marketing/src/pages/index.astro+26−2
@@ -43,10 +43,14 @@ import { AnimatedBeamDemo } from '../components/animated-beam-demo' · 42 unmodified lines
4343 </p>
4444 <div class="rise-4 flex flex-wrap items-center justify-center gap-5">
4545 <a href="/cloud" class="btn-primary">Try Cloud →</a>
46− <a href="#install" class="btn-link">
46+ <button type="button" class="btn-link copy-btn" data-copy="npx executor web" aria-label="Copy npx executor web to clipboard">
4747 <span class="font-mono text-[13px] text-ink-3">$</span>
4848 <span>npx executor web</span>
49− </a>
49+ <span class="copy-icons text-ink-3" aria-hidden="true">
50+ <svg class="copy-icon-copy" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
51+ <svg class="copy-icon-check" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
52+ </span>
53+ </button>
5054 </div>
5155 </div>
5256 </div>
@@ -291,3 +295,23 @@ import { AnimatedBeamDemo } from '../components/animated-beam-demo' · 238 unmodified lines
291295 </div>
292296 </main>
293297 </Layout>
298+
299+<script>
300+ document.querySelectorAll<HTMLButtonElement>('button[data-copy]').forEach((btn) => {
301+ let timer: ReturnType<typeof setTimeout> | null = null
302+
303+ btn.addEventListener('click', async () => {
304+ const text = btn.dataset.copy ?? ''
305+ try {
306+ await navigator.clipboard.writeText(text)
307+ btn.dataset.copied = 'true'
308+ } catch {
309+ btn.dataset.copied = 'false'
310+ }
311+ if (timer) clearTimeout(timer)
312+ timer = setTimeout(() => {
313+ delete btn.dataset.copied
314+ }, 1500)
315+ })
316+ })
317+</script>
apps/marketing/src/styles/global.css+34−2
@@ -55,8 +55,9 @@ body { · 54 unmodified lines
5555 z-index: 1;
5656 }
5757 .pattern-graph {
58− -webkit-mask-image: url("/pattern-graph-paper.svg");
59− mask-image: url("/pattern-graph-paper.svg");
58+ --pattern-graph: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='100'%20height='100'%20viewBox='0%200%20100%20100'%3E%3Cg%20fill-rule='evenodd'%3E%3Cg%20fill='%23000'%3E%3Cpath%20opacity='.5'%20d='M96%2095h4v1h-4v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4h-9v4h-1v-4H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15v-9H0v-1h15V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h9V0h1v15h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9h4v1h-4v9zm-1%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-9-10h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm9-10v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-9-10h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm9-10v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-9-10h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm9-10v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-10%200v-9h-9v9h9zm-9-10h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9zm10%200h9v-9h-9v9z'/%3E%3Cpath%20d='M6%205V0H5v5H0v1h5v94h1V6h94V5H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
59+ -webkit-mask-image: var(--pattern-graph);
60+ mask-image: var(--pattern-graph);
6061 -webkit-mask-size: 100px 100px;
6162 mask-size: 100px 100px;
6263 opacity: 0.08;
@@ -141,11 +142,42 @@ body { · 78 unmodified lines
141142 align-items: center;
142143 gap: 0.4rem;
143144 transition: color 0.15s ease;
145+ background: transparent;
146+ border: 0;
147+ padding: 0;
148+ font-family: inherit;
149+ cursor: pointer;
144150 }
145151 .btn-link:hover {
146152 color: var(--color-ink);
147153 }
148154
155+.copy-icons {
156+ position: relative;
157+ display: inline-flex;
158+ width: 14px;
159+ height: 14px;
160+ margin-left: 0.15rem;
161+}
162+.copy-icons > svg {
163+ position: absolute;
164+ inset: 0;
165+ transition: opacity 0.15s ease, transform 0.15s ease;
166+}
167+.copy-icon-check {
168+ opacity: 0;
169+ transform: scale(0.8);
170+ color: var(--color-accent);
171+}
172+.copy-btn[data-copied="true"] .copy-icon-copy {
173+ opacity: 0;
174+ transform: scale(0.8);
175+}
176+.copy-btn[data-copied="true"] .copy-icon-check {
177+ opacity: 1;
178+ transform: scale(1);
179+}
180+
149181 /* Legal pages */
150182 .legal-prose {
151183 color: color-mix(in srgb, var(--color-ink) 85%, transparent);