diff --git a/app/src/components/ClientApp.astro b/app/src/components/ClientApp.astro index cb14243..2a1ffe0 100644 --- a/app/src/components/ClientApp.astro +++ b/app/src/components/ClientApp.astro @@ -11,6 +11,271 @@ observeSpecIndex(); observeFocusBand(); + // Share popup — confetti rains across the viewport on sign, the user's + // rank is stamped, an identity statement follows, the share phrase lives + // in a quotation block (with a clickable link to ai-driven-development.org), + // then a countdown announces the GitHub PR opening. The action row carries + // X (sized popup, Twitter pre-fills via ?text=), Copy (clipboard), and + // LinkedIn — which is always visible. LinkedIn click uses `navigator.share()` + // when available (reliable pre-fill) or falls back to clipboard-copy + + // LinkedIn popup + a "paste here" hint (Firefox / older browsers in 2025 + // still lack Web Share but can Copy → Cmd+V). Message language tracks + // navigator.language. + const SHARE_URL = 'https://www.ai-driven-development.org/'; + const URL_LABEL = 'ai-driven-development.org'; + const URL_TOKEN = '\u0001URL\u0001'; + let shareCountdownInterval: ReturnType | null = null; + + function isFrench(lang: string) { return lang.toLowerCase().startsWith('fr'); } + + function ordinal(n: number, fr: boolean): string { + if (fr) return n === 1 ? '1er' : `${n}ème`; + const j = n % 10, k = n % 100; + if (j === 1 && k !== 11) return `${n}st`; + if (j === 2 && k !== 12) return `${n}nd`; + if (j === 3 && k !== 13) return `${n}rd`; + return `${n}th`; + } + + function buildStatement(rank: number, lang: string): string { + const fr = isFrench(lang); + const ord = ordinal(rank, fr); + return fr ? `Vous êtes le ${ord} AI-Driven Developer` : `You are the ${ord} AI-Driven Developer`; + } + + function buildMessagePlain(rank: number, lang: string): string { + const fr = isFrench(lang); + const line1 = fr + ? `Je viens de signer le Manifeste de l'AI-Driven Development en tant que signataire n°${rank}.` + : `I just signed the AI-Driven Development Manifesto as signatory #${rank}.`; + const line2 = URL_TOKEN; + return `${line1}\n${line2}`.replaceAll(URL_TOKEN, SHARE_URL); + } + + function buildMessageHTML(rank: number, lang: string): string { + const fr = isFrench(lang); + const line1 = fr + ? `Je viens de signer le Manifeste de l'AI-Driven Development en tant que signataire n°${rank}.` + : `I just signed the AI-Driven Development Manifesto as signatory #${rank}.`; + const line2 = URL_TOKEN; + const anchor = `${URL_LABEL}`; + return `${line1}
${line2}`.replaceAll(URL_TOKEN, anchor); + } + + function resetShareButtons(ready: boolean) { + const buttons = document.querySelector('.share-popup-buttons'); + if (!buttons) return; + buttons.setAttribute('data-ready', String(ready)); + document.querySelectorAll('.share-popup-btn').forEach((b) => { + if (b.tagName === 'A') { + b.setAttribute('aria-disabled', String(!ready)); + } else { + (b as HTMLButtonElement).disabled = !ready; + } + }); + } + + function renderCountdown(el: HTMLElement, n: number, lang: string) { + const fr = isFrench(lang); + const label = fr ? 'Ouverture de votre contribution sur GitHub dans' : 'Opening your contribution on GitHub in'; + el.innerHTML = `${label} ${n}`; + } + + // Confetti emojis — rain across the whole viewport on sign, so the burst + // is visible even with the dialog open (they layer above the scrim). + const CONFETTI_EMOJIS = ['🎉', '✨', '🚀', '💻', '🌟', '💙', '⭐', '🎊', '🔥', '✅']; + function launchConfetti() { + if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return; + const vw = window.innerWidth; + const vh = window.innerHeight; + const count = 64; + for (let i = 0; i < count; i++) { + const el = document.createElement('span'); + el.className = 'confetti-emoji'; + el.textContent = CONFETTI_EMOJIS[i % CONFETTI_EMOJIS.length]; + // Origin spread across the full top band + a few from each side. + const fromTop = i % 3 !== 2; + const startX = fromTop ? Math.random() * vw : (i % 4 === 0 ? -20 : vw + 20); + const startY = fromTop ? -20 - Math.random() * 40 : Math.random() * vh * 0.6; + // Drift downward and outward; long flight so the celebration lingers. + const drift = 60 + Math.random() * 180; + const dx = (Math.random() - 0.5) * 220; + const dy = fromTop ? vh * 0.5 + drift : drift * 0.6; + el.style.left = `${startX}px`; + el.style.top = `${startY}px`; + el.style.setProperty('--dx', `${dx}px`); + el.style.setProperty('--dy', `${dy}px`); + el.style.setProperty('--rot', `${Math.random() * 900 - 450}deg`); + el.style.setProperty('--delay', `${Math.random() * 0.35}s`); + // Vary size slightly for depth. + el.style.fontSize = `${28 + Math.random() * 16}px`; + document.body.appendChild(el); + el.addEventListener('animationend', () => el.remove(), { once: true }); + } + } + + function startShareCountdown(githubUrl: string) { + const popup = document.getElementById('share-popup') as HTMLDialogElement | null; + const countdownEl = document.getElementById('share-countdown'); + const rankEl = document.getElementById('share-popup-rank'); + const statementEl = document.getElementById('share-popup-statement'); + const textEl = document.getElementById('share-popup-title'); + const copyFeedback = document.getElementById('share-copy-feedback'); + + if (!popup || !countdownEl) return; + + if (shareCountdownInterval) { + clearInterval(shareCountdownInterval); + shareCountdownInterval = null; + } + + const rank = Number(popup.dataset.signatoryRank) || 1; + const lang = navigator.language || 'en'; + const plain = buildMessagePlain(rank, lang); + const html = buildMessageHTML(rank, lang); + + if (rankEl) rankEl.textContent = `#${rank}`; + if (statementEl) statementEl.textContent = buildStatement(rank, lang); + if (textEl) textEl.innerHTML = html; + if (copyFeedback) copyFeedback.hidden = true; + + const xHref = `https://twitter.com/intent/tweet?text=${encodeURIComponent(plain)}`; + document.getElementById('share-btn-x')?.setAttribute('href', xHref); + // LinkedIn is a ` + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/components/signature/SignButton.astro b/app/src/components/signature/SignButton.astro index 0d39d86..fd12a81 100644 --- a/app/src/components/signature/SignButton.astro +++ b/app/src/components/signature/SignButton.astro @@ -14,6 +14,7 @@ statement: # optional - one-line public statement (max 280 chars) `; const HREF = `https://github.com/${REPO}/new/main/${TARGET_PATH}?filename=YOUR-HANDLE.yml&value=${encodeURIComponent(TEMPLATE)}`; +const GITHUB_URL = HREF; interface Props { /** 'github' (default, bottom CTA) or 'aidd' (animated brand mark, sidebar). */ @@ -23,7 +24,13 @@ interface Props { } const { logo = 'github', variant = 'default' } = Astro.props; --- - + diff --git a/app/src/styles/sections/principles.css b/app/src/styles/sections/principles.css index 893eb33..4318d2c 100644 --- a/app/src/styles/sections/principles.css +++ b/app/src/styles/sections/principles.css @@ -23,11 +23,12 @@ --row-inset: clamp(18px, 2.2vw, 28px); position: relative; display: grid; - grid-template-columns: clamp(44px, 4.6vw, 58px) minmax(320px, 1fr) minmax(280px, 380px); - gap: clamp(18px, 1.9vw, 28px); + grid-template-columns: clamp(48px, 5vw, 64px) minmax(0, 1fr) minmax(280px, 420px); + gap: clamp(18px, 2vw, 28px); align-items: start; min-width: 0; - padding: clamp(26px, 4.3vh, 46px) var(--row-inset); + /* Generous breathing room — rows were too cramped. */ + padding: clamp(40px, 6.5vh, 78px) var(--row-inset); scroll-margin-top: clamp(20px, 8vh, 80px); border-bottom: 1px solid var(--rule); background: var(--paper); diff --git a/app/src/styles/sections/signature.css b/app/src/styles/sections/signature.css index 26e6490..a2b3418 100644 --- a/app/src/styles/sections/signature.css +++ b/app/src/styles/sections/signature.css @@ -701,3 +701,32 @@ @media (prefers-reduced-motion: reduce){ .sign-btn-sig path{ animation: none; stroke-dashoffset: 0; } } + +/* ---- Share popup confetti emojis — rain across the whole viewport on sign ---- */ +.confetti-emoji{ + position: fixed; + z-index: 1200; /* above the popup (1000) + scrim */ + font-size: 34px; + line-height: 1; + pointer-events: none; + user-select: none; + transform: translate(-50%, -50%); + animation: confetti-fall 1.9s var(--ease-out) var(--delay, 0s) forwards; + will-change: transform, opacity; + filter: drop-shadow(0 2px 4px oklch(0 0 0 / 0.18)); +} +@keyframes confetti-fall{ + 0%{ + transform: translate(-50%, -50%) rotate(0deg) scale(0.4); + opacity: 0; + } + 12%{ opacity: 1; transform: translate(-50%, -50%) rotate(var(--rot)) scale(1); } + 80%{ opacity: 1; } + 100%{ + transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy))) rotate(var(--rot)) scale(1.1); + opacity: 0; + } +} +@media (prefers-reduced-motion: reduce){ + .confetti-emoji{ animation: none; display: none; } +} diff --git a/app/src/styles/tokens.css b/app/src/styles/tokens.css index d65eab6..096eb43 100644 --- a/app/src/styles/tokens.css +++ b/app/src/styles/tokens.css @@ -24,6 +24,10 @@ --mint: oklch(0.72 0.13 162); --raspberry: oklch(0.60 0.18 9); + /* External platform brand fills — used only on the share popup buttons. */ + --x-brand: oklch(0 0 0); /* X / Twitter — pure black */ + --linkedin-brand: oklch(0.519 0.174 258); /* #0A66C2 — LinkedIn blue */ + /* Type — characterful display + clean sans. Mono only inside code illustrations. */ --display: "Bricolage Grotesque", "Inter Tight", ui-sans-serif, system-ui, sans-serif; --sans: "Inter Tight", ui-sans-serif, system-ui, sans-serif;