Skip to content

Render the waitlist form natively so it themes in light and dark - #14

Merged
cmin764 merged 11 commits into
mainfrom
feat/themed-waitlist-form
Sep 9, 2026
Merged

cmin764 merged 11 commits into
mainfrom
feat/themed-waitlist-form

Conversation

@cmin764

@cmin764 cmin764 commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Supersedes #13 (same branch/commit history plus fixes from review). Original PR by @qxZap: renders the waitlist form as our own markup instead of a cross-origin Tally iframe, so it themes in light and dark. Every route to CSS-theme or drive the iframe was tested and closed; details in #13 and docs/dev-guide.md.

On top of the original commit

  • Timeout on the submit request. Without one, a hung fetch (not a rejected one, a stalled one) left the button on "Joining…" forever with no error ever shown, which the PR's own rule says can't happen. AbortSignal.timeout(15_000) on the request fixes it.
  • Dropped a duplicate .sr-only rule. Tailwind v4 already emits it (@import "tailwindcss" plus the class being used is enough), so the hand-written copy in index.css was dead weight.
  • Dropped .dockerignore. compose.yaml bind-mounts the repo with no build: step, so the file was never read.
  • Real message on a duplicate submission. Tally rejects a repeat submission with FORM_UNIQUE_SUBMISSION_CONFLICT; the form now shows that reason instead of the generic "went wrong on our side" error, and drops the mailto: fallback since there's nothing to report.
  • Docs sync: docs/site-copy.md and docs/build-plan.md still described the old six-role Tally-hosted field set; updated to match waitlist.form.* / TALLY_ROLE_OPTIONS.

Verified

  • bun run typecheck, lint, build: clean.
  • TALLY_FIELDS / TALLY_ROLE_OPTIONS UUIDs checked against the live form definition at tally.so/embed/xXvOJk: exact match on all 4 field UUIDs and all 3 role option UUIDs/labels.
  • CORS preflight against api.tally.so/forms/xXvOJk/respond for both prod and localhost origins: 204, origin reflected.
  • Browser (light + dark): empty submit shows all 4 errors, focuses the first invalid field, fires zero network requests; a valid submission returns 200 and routes client-side to /thank-you; a forced network failure shows the error with the mailto: fallback, keeps typed values, and re-enables the button.
  • Duplicate submission reproduced directly against the live endpoint and through the form: shows the correct message, no mailto: link, form data preserved.

To watch

  • The undocumented Tally endpoint could change shape without notice; the failure path is designed to degrade loudly if it does.
  • TALLY_FIELDS / TALLY_ROLE_OPTIONS are a manual contract with the Tally dashboard: editing the form there requires re-reading the UUIDs (documented in src/config.ts).

qxZap and others added 4 commits September 8, 2026 14:58
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/<id>/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) <noreply@anthropic.com>
@cmin764 cmin764 self-assigned this Sep 9, 2026
@vercel

vercel Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
traced-ai-website Ready Ready Preview Sep 9, 2026 9:48am UTC

Tally rejects a repeat submission from the same respondent with
FORM_UNIQUE_SUBMISSION_CONFLICT, which the form was reporting as the
generic "something went wrong on our side" error. Detect it and show
the actual reason instead, without the mailto fallback since there's
nothing to report.
In case the visitor skipped booking the first time. Reuses CAL_BOOKING_URL
and the same outbound-link pattern as the /thank-you CTA.
Accent color, bold, underline, so it reads as clickable against the red
error text instead of blending in.
whiteSpace: nowrap on the anchor stops the arrow orphaning onto its own
line at narrow widths.
One canonical file for Claude and Codex project instructions instead of
a manually-copied duplicate that can drift.
AGENTS.md is the tool-neutral convention multiple agents look for;
CLAUDE.md becomes the pointer instead. Reworded the intro line to
not name Claude Code specifically.
Matches the repo's own @-import convention (see @docs/dev-guide.md
below) and avoids symlink quirks in tools that don't resolve them
(GitHub raw/zip, Windows checkouts).
@cmin764
cmin764 merged commit 6fcc877 into main Sep 9, 2026
3 checks passed
@cmin764
cmin764 deleted the feat/themed-waitlist-form branch September 9, 2026 09:50

This branch was successfully deployed

1 active deployment
Preview — bed73331 Deployed Sep 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants