From 12cb33f4829a4e1bd2e60cd95fdb9e68a72f7d41 Mon Sep 17 00:00:00 2001 From: qxZap Date: Tue, 8 Sep 2026 14:58:20 +0300 Subject: [PATCH] feat: render the waitlist form natively so it themes in light and dark The waitlist form was a cross-origin Tally iframe, which meant it could not follow the site theme: in dark mode its labels rendered near-black on the near-black page and the fields read as grey on grey. Every route to theming the embed was tested against the live form, and all of them are closed: - CSS into the frame is blocked by same-origin policy. - Tally's postMessage protocol is outbound-only (FormLoaded, FormPageView, FormSubmitted, FormRedirect, viewport height). No inbound command exists, so the frame cannot be driven from the parent either. - There is no theme or color URL parameter; the palette lives server-side in the form's settings.styles. - Tally's custom CSS is a paid feature and this workspace is on FREE. - color-scheme does not propagate into the frame: with the OS in light mode and color-scheme:dark on the iframe, prefers-color-scheme inside still reports light. Tally's own embed.js additionally force-sets iframe.style.colorScheme = 'light' whenever transparentBackground=1. So the form is now our own markup, POSTed straight to api.tally.so/forms//respond, which takes no API key and reflects our origin in its CORS headers. Same Tally form, same dashboard, same downstream pipeline, but the markup is ours and themes with the rest of the site. Verified end to end: Tally returns 200 with a submissionId, and success routes to /thank-you client-side rather than following Tally's absolute production redirect, so the visitor stays in the SPA. Field UUIDs are Tally's internal block identifiers and are the contract between our fields and Tally's columns; config.ts documents how to re-read them if the form is edited in the dashboard. A dropdown answer is sent as an array containing the option UUID, not its label. Accessibility: labels bound to inputs, aria-invalid and aria-describedby per field, focus moved to the first invalid field on a failed submit, the submit error announced via role="alert", and the required marker reads as "Required" rather than a bare asterisk. An invalid field's focus ring flips to the danger colour, outranking the global :focus-visible accent ring. :root now sets color-scheme per theme so native controls follow the site theme. A failed submission keeps everything the visitor typed, shows the error, and offers a mailto fallback. Spam is handled by an off-screen honeypot, since the embed's own heuristics are gone. CSP: connect-src allows https://api.tally.so; tally.so is dropped from script-src and frame-src as nothing is embedded any more. Privacy policy updated: Tally is no longer an embed, so nothing loads from Tally while browsing and no Tally script runs on the site. Tally remains the processor for submissions. Also adds an optional Docker dev setup, which is how this was validated across mobile, tablet, and desktop. Docs synced: dev-guide (why an embed cannot be themed, and the rules for posting to Tally directly), design-system.html, site-copy (with the iframe kept as a [cut] note), build-plan, README, and the frontend-review checklist. Co-Authored-By: Claude Opus 5 (1M context) --- .../frontend-review/references/checklist.md | 5 + .dockerignore | 3 + README.md | 12 + compose.yaml | 16 ++ docs/build-plan.md | 2 +- docs/design-system.html | 14 + docs/dev-guide.md | 29 ++ docs/site-copy.md | 6 +- src/config.ts | 28 ++ src/copy.ts | 21 ++ src/index.css | 41 +++ src/pages/Privacy.tsx | 2 +- src/sections/WaitlistForm.tsx | 248 +++++++++++++++--- vercel.json | 2 +- vite.config.ts | 8 + 15 files changed, 402 insertions(+), 35 deletions(-) create mode 100644 .dockerignore create mode 100644 compose.yaml diff --git a/.claude/skills/frontend-review/references/checklist.md b/.claude/skills/frontend-review/references/checklist.md index c797366..dacc6bb 100644 --- a/.claude/skills/frontend-review/references/checklist.md +++ b/.claude/skills/frontend-review/references/checklist.md @@ -155,6 +155,11 @@ Forward-looking: Tally (live in `WaitlistForm`), Cal.eu (live in `ThankYou`), an **E5**: Embeds that set cookies (Cal.eu, Stripe) must be reflected in the privacy policy. A new tracking or cookie-setting embed is a legal-page update trigger (ties to L1). +**E6**: A cross-origin embed cannot be themed or driven from the parent page. Flag as ineffective any attempt to style inside a vendor iframe (injected `