From 33060802bc49b3657154b5dbd3e907c7c2e1f604 Mon Sep 17 00:00:00 2001 From: alexsoyes Date: Wed, 24 Jun 2026 13:42:01 +0200 Subject: [PATCH 1/8] Refine cover and signature layouts --- app/src/styles/sections/principles.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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); From 67cb4c44f78428f22c0fabbb45801abf8bcc4f7d Mon Sep 17 00:00:00 2001 From: alexsoyes Date: Sat, 27 Jun 2026 11:54:25 +0200 Subject: [PATCH 2/8] feat(signature): add share popup with confetti, rank badge, and platform share Adds a SharePopup.astro modal that opens after clicking the redesigned "Sign and share" button (SignButton.astro is now 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/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; From ff2a89e3cb4e8db340d73b318d92389eb4c94daf Mon Sep 17 00:00:00 2001 From: alexsoyes Date: Sat, 27 Jun 2026 16:56:58 +0200 Subject: [PATCH 3/8] fix(share): use LinkedIn shareArticle with mini=true + inline title/summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit share-offsite redirects internally to shareArticle but strips every query param except url — the popup opens with a bare link, no pre-filled post text, and no compact popup affordance. Switch to shareArticle directly with mini=true (compact popup), url (canonical www URL), title (mirrors og:title, localized FR/EN), summary (pre-fills the post with the full share message — same UX as X's text= param), and source (bare domain). Also add og:image:type and og:image:alt to Page.astro — strengthens the LinkedIn Open Graph preview card. --- app/src/components/ClientApp.astro | 14 ++++++++++++-- app/src/components/layout/Page.astro | 2 ++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/components/ClientApp.astro b/app/src/components/ClientApp.astro index e6838f6..42a8603 100644 --- a/app/src/components/ClientApp.astro +++ b/app/src/components/ClientApp.astro @@ -17,7 +17,7 @@ // then a countdown announces the GitHub PR opening. The Copy pill sits in // the action row beside X and LinkedIn. Message language tracks // navigator.language. - const SHARE_URL = 'https://ai-driven-development.org/'; + 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; @@ -136,7 +136,17 @@ if (copyFeedback) copyFeedback.hidden = true; const xHref = `https://twitter.com/intent/tweet?text=${encodeURIComponent(plain)}`; - const inHref = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(SHARE_URL)}`; + // shareArticle (not share-offsite) so the popup is compact (mini=true) and + // the post text is pre-filled via the summary param — share-offsite strips + // everything but url and shows a bare link. Title mirrors og:title; source + // is the bare domain. All values URL-encoded. + const fr = isFrench(lang); + const inTitle = encodeURIComponent( + fr ? 'Le Manifeste du AI-Driven Development' : 'The Manifesto for AI-Driven Development' + ); + const inSummary = encodeURIComponent(plain); + const inSource = encodeURIComponent(URL_LABEL); + const inHref = `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(SHARE_URL)}&title=${inTitle}&summary=${inSummary}&source=${inSource}`; document.getElementById('share-btn-x')?.setAttribute('href', xHref); document.getElementById('share-btn-linkedin')?.setAttribute('href', inHref); diff --git a/app/src/components/layout/Page.astro b/app/src/components/layout/Page.astro index fb5ff8f..097f0c5 100644 --- a/app/src/components/layout/Page.astro +++ b/app/src/components/layout/Page.astro @@ -74,6 +74,8 @@ const pageTitle = title === SITE.name ? title : `${title} | ${SITE.shortName}`; + + From 0c75c2492590824550a5e5a4c0046fbb028d4b2e Mon Sep 17 00:00:00 2001 From: alexsoyes Date: Sat, 27 Jun 2026 20:59:03 +0200 Subject: [PATCH 4/8] fix(share): flat summary + sized popup for reliable LinkedIn composer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LinkedIn's composer has long been documented (SO q/56373068, q/51189486) to render the shareArticle `summary` param inconsistently — and may silently drop it entirely when the value contains a literal newline (`%0A`). Our previous summary was two lines joined by `\n` (the Clipboard / X context expects multiline), so when the shareArticle popup opened post-login, the post body appeared empty. Fix: build a separate flat variant of the share message that uses an em-dash separator on a single line (`buildMessageFlat`), used only for the LinkedIn `summary` param. X's intent/tweet?text= and the Copy pill keep the original multi-line text (Twitter renders it reliably). Also intercept clicks on the X and LinkedIn share buttons and open them via `window.open(href, 'aidd-share', 'width=750,height=620')`. The 750×620 size matches react-share's LinkedinShareButton and lets LinkedIn's `mini=true` flag actually pick its compact share layout — opening the same href as a plain target=_blank tab falls through to the default 200 signatories list view and looks 'broken' to the user. Sources: - react-share master (LinkedinShareButton.tsx) - Microsoft Learn, Share Plugin (last updated 2022-03-31) confirms shareArticle endpoint - Empirical curl -I (2026-06-27) confirms shareArticle preserves all params through the login 302; share-offsite strips everything except `url`. --- app/src/components/ClientApp.astro | 39 ++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/app/src/components/ClientApp.astro b/app/src/components/ClientApp.astro index 42a8603..2ddb222 100644 --- a/app/src/components/ClientApp.astro +++ b/app/src/components/ClientApp.astro @@ -48,6 +48,20 @@ return `${line1}\n${line2}`.replaceAll(URL_TOKEN, SHARE_URL); } + // Single-line variant for LinkedIn shareArticle summary — LinkedIn's composer + // historically renders `%0A` (newline) inconsistently (SO q/56373068, + // q/51189486) and may silently drop the summary entirely when it contains + // one. Use a dash separator on a single line so LinkedIn reliably pre-fills + // the post body. + function buildMessageFlat(rank: number, lang: string): string { + const fr = isFrench(lang); + const line1 = fr + ? `Je viens de signer le Manifeste du AI-Driven Development en tant que signataire n°${rank}.` + : `I just signed the AI-Driven Development Manifesto as signatory #${rank}.`; + const line2 = fr ? `Rejoignez-moi → ${URL_TOKEN}` : `Join me → ${URL_TOKEN}`; + return `${line1} — ${line2}`.replaceAll(URL_TOKEN, SHARE_URL); + } + function buildMessageHTML(rank: number, lang: string): string { const fr = isFrench(lang); const line1 = fr @@ -136,15 +150,18 @@ if (copyFeedback) copyFeedback.hidden = true; const xHref = `https://twitter.com/intent/tweet?text=${encodeURIComponent(plain)}`; - // shareArticle (not share-offsite) so the popup is compact (mini=true) and - // the post text is pre-filled via the summary param — share-offsite strips - // everything but url and shows a bare link. Title mirrors og:title; source - // is the bare domain. All values URL-encoded. + // shareArticle (not share-offsite) preserves the title/summary/source + // params through the login redirect — share-offsite strips everything + // except url. Summary MUST be a single line: LinkedIn's composer has + // historically rendered `%0A` inconsistently and can silently drop the + // entire summary when it contains a newline (SO q/56373068, q/51189486). + // We use a flat variant with a dash separator for that reason. + // Title mirrors og:title; source is the bare domain. const fr = isFrench(lang); const inTitle = encodeURIComponent( fr ? 'Le Manifeste du AI-Driven Development' : 'The Manifesto for AI-Driven Development' ); - const inSummary = encodeURIComponent(plain); + const inSummary = encodeURIComponent(buildMessageFlat(rank, lang)); const inSource = encodeURIComponent(URL_LABEL); const inHref = `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(SHARE_URL)}&title=${inTitle}&summary=${inSummary}&source=${inSource}`; document.getElementById('share-btn-x')?.setAttribute('href', xHref); @@ -210,6 +227,18 @@ e.preventDefault(); const githubUrl = (signBtn as HTMLElement).dataset.githubUrl || ''; startShareCountdown(githubUrl); + return; + } + // Open X / LinkedIn share links in a compact popup (750x600) instead of a + // background tab. react-share uses this size; the LinkedIn shareArticle + // endpoint's `mini=true` hints at the same intent but only fires when the + // window is actually sized like a popup, not a full browser tab. + const shareLink = (e.target as HTMLElement).closest('#share-btn-x, #share-btn-linkedin'); + if (shareLink && shareLink.getAttribute('aria-disabled') !== 'true') { + e.preventDefault(); + const href = shareLink.getAttribute('href') || '#'; + const features = 'noopener,noreferrer,width=750,height=620,scrollbars=yes'; + window.open(href, 'aidd-share', features); } }); From 1ebae87b0a98cf4fa65c6e0763b56d60356f0300 Mon Sep 17 00:00:00 2001 From: alexsoyes Date: Sat, 27 Jun 2026 21:42:24 +0200 Subject: [PATCH 5/8] fix(share): LinkedIn button uses Web Share API then clipboard+hint fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LinkedIn has historically dropped the shareArticle `summary` param during pre-fill (SO q/56373068, q/51189486 — long-standing complaints now confirmed by LinkedIn removing all non-OAuth share docs). The composer body opens empty even when the URL carries summary=title=. The official IN/Share plugin exposes only `data-url` — no text/title/ summary attribute exists — confirming LinkedIn no longer accepts URL-based composer text pre-fill. New strategy for the LinkedIn button: 1. **Web Share API first** — if `navigator.share({text, url, title})` is available (iOS/Android, modern Chrome/Edge desktop), call it. The native OS share sheet pre-fills LinkedIn's composer body `text` reliably — this is the only path that still works end-to-end in 2025. 2. **Clipboard + popup + hint fallback** — if Web Share isn't available, copy the share message to the clipboard, open the LinkedIn popup (750×620, shareArticle url-only), and surface a small hint next to the button: 'Message copied — paste it on LinkedIn' (FR: 'Message copié — collez-le sur LinkedIn'). The user is one Cmd/Ctrl-V away from the share text being in the composer. X button stays unchanged — Twitter reliably honors `?text=` including newlines, so the public body is pre-filled as before. Files: - ClientApp.astro: dodge Web Share detection via `navigator.share` + `navigator.canShare`, async clipboard.writeText with try/catch so the popup opens regardless of permission. - SharePopup.astro: add `#share-linkedin-hint` element + styling (LinkedIn blue tinted pill, distinct from the green copy-confirmation feedback). --- app/src/components/ClientApp.astro | 62 ++++++++++++++++--- app/src/components/signature/SharePopup.astro | 17 +++++ 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/app/src/components/ClientApp.astro b/app/src/components/ClientApp.astro index 2ddb222..b9ba7db 100644 --- a/app/src/components/ClientApp.astro +++ b/app/src/components/ClientApp.astro @@ -229,16 +229,60 @@ startShareCountdown(githubUrl); return; } - // Open X / LinkedIn share links in a compact popup (750x600) instead of a - // background tab. react-share uses this size; the LinkedIn shareArticle - // endpoint's `mini=true` hints at the same intent but only fires when the - // window is actually sized like a popup, not a full browser tab. - const shareLink = (e.target as HTMLElement).closest('#share-btn-x, #share-btn-linkedin'); - if (shareLink && shareLink.getAttribute('aria-disabled') !== 'true') { + // X button — opens in a sized popup, Twitter reliably pre-fills the + // composer body from the `text=` query param. + const xLink = (e.target as HTMLElement).closest('#share-btn-x'); + if (xLink && xLink.getAttribute('aria-disabled') !== 'true') { e.preventDefault(); - const href = shareLink.getAttribute('href') || '#'; - const features = 'noopener,noreferrer,width=750,height=620,scrollbars=yes'; - window.open(href, 'aidd-share', features); + window.open(xLink.getAttribute('href') || '#', 'aidd-share-x', 'noopener,noreferrer,width=750,height=620,scrollbars=yes'); + return; + } + // LinkedIn button — LinkedIn has deprecated pre-filling the composer body + // via URL params. `shareArticle?summary=...` is silently ignored in the + // current composer (SO q/56373068 etc., confirmed: the official IN/Share + // plugin exposes only `data-url`). Strategy: + // 1. Web Share API (mobile + Chrome/Edge desktop) → opens the OS share + // sheet, which DOES pre-fill LinkedIn's composer `text` reliably + // (the OS share target contract carries `text` + `url`). + // 2. Fallback → copy the message to clipboard, open the LinkedIn popup + // with `url` only, and surface a "paste it in LinkedIn" hint next to + // the button so the user knows the body is already in their + // clipboard. + const liLink = (e.target as HTMLElement).closest('#share-btn-linkedin'); + if (liLink && liLink.getAttribute('aria-disabled') !== 'true') { + e.preventDefault(); + const href = liLink.getAttribute('href') || '#'; + const popup = document.getElementById('share-popup') as HTMLDialogElement | null; + const rank = popup ? Number(popup.dataset.signatoryRank) || 1 : 1; + const lang = navigator.language || 'en'; + const fr = isFrench(lang); + const message = buildMessagePlain(rank, lang); + const title = fr ? 'Le Manifeste du AI-Driven Development' : 'The Manifesto for AI-Driven Development'; + const nav = navigator as Navigator & { share?: (d: unknown) => Promise; canShare?: (d: unknown) => boolean }; + if (typeof nav.share === 'function' && (typeof nav.canShare !== 'function' || nav.canShare({ text: message, url: SHARE_URL }))) { + nav.share({ text: message, url: SHARE_URL, title }).catch(() => { + // User dismissed or denied — fall back to popup. + window.open(href, 'aidd-share-li', 'noopener,noreferrer,width=750,height=620,scrollbars=yes'); + }); + return; + } + // Fallback — silently copy to clipboard, then open the popup, then + // show a hint. Wrap clipboard call in try/catch so the popup opens + // regardless of clipboard permission. + (async () => { + let copied = false; + try { await navigator.clipboard.writeText(message); copied = true; } catch {} + const features = 'noopener,noreferrer,width=750,height=620,scrollbars=yes'; + window.open(href, 'aidd-share-li', features); + if (copied) { + const hint = document.getElementById('share-linkedin-hint'); + if (hint) { + hint.textContent = fr ? 'Message copié — collez-le sur LinkedIn' : 'Message copied — paste it on LinkedIn'; + hint.hidden = false; + setTimeout(() => { hint.hidden = true; }, 4000); + } + } + })(); } }); diff --git a/app/src/components/signature/SharePopup.astro b/app/src/components/signature/SharePopup.astro index 869c26d..b6ceebb 100644 --- a/app/src/components/signature/SharePopup.astro +++ b/app/src/components/signature/SharePopup.astro @@ -91,6 +91,7 @@ const { rank } = Astro.props; + @@ -343,6 +344,22 @@ const { rank } = Astro.props; color: var(--accent); } + /* LinkedIn "paste here" hint — distinct from the copy-confirm feedback: + this is an action-guidance message, so it gets the LinkedIn blue and + sits alongside the buttons to encourage the paste action. */ + .share-popup-linkedin-hint { + color: var(--linkedin-brand); + border: 1px solid oklch(from var(--linkedin-brand) l c h / 0.32); + background: oklch(from var(--linkedin-brand) l c h / 0.08); + border-radius: var(--radius-pill); + padding: 8px 14px; + margin: 10px auto 0; + max-width: min(90%, 380px); + line-height: 1.4; + font-weight: 600; + animation: share-popup-in var(--dur-med) var(--ease-out); + } + @keyframes share-popup-in { from { opacity: 0; transform: scale(0.96) translateY(-12px); } to { opacity: 1; transform: scale(1) translateY(0); } From 08d653a58c2dc109f63f94680536176290be3bd4 Mon Sep 17 00:00:00 2001 From: alexsoyes Date: Sat, 27 Jun 2026 22:14:51 +0200 Subject: [PATCH 6/8] refactor(share): use only native Web Share API for LinkedIn, hide if unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LinkedIn has deprecated URL-based composer pre-fill. The only path that still reliably pre-fills LinkedIn's composer body in 2025 is the native Web Share API (the OS share sheet carries `text` + `url`, and LinkedIn's share target honors both). Replace the previous strategy (Web Share → clipboard + popup + hint fallback) with a clean single approach: - LinkedIn button is a `