Skip to content

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

Closed
qxZap wants to merge 1 commit into
Traced-AI:mainfrom
qxZap:feat/themed-waitlist-form
Closed

qxZap wants to merge 1 commit into
Traced-AI:mainfrom
qxZap:feat/themed-waitlist-form

Conversation

@qxZap

@qxZap qxZap commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The problem

The waitlist form was a cross-origin Tally iframe, so 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.

Why the iframe could not be fixed

Every route was tested against the live embed. All are closed:

Route Result
CSS into the frame Blocked by same-origin policy
Drive it via postMessage Tally's protocol is outbound-only (FormLoaded, FormPageView, FormSubmitted, FormRedirect, viewport height). No inbound command exists
Synthesize clicks or typing Cross-origin: cannot focus, dispatch events, or reach any element inside
Theme / colour URL parameter None. The palette lives server-side in the form's settings.styles
Tally's custom CSS Paid feature; this workspace is on FREE
Propagate color-scheme into the frame Does not propagate. OS light + color-scheme: dark on the iframe → prefers-color-scheme inside still reports light. Tally's embed.js also force-sets iframe.style.colorScheme = 'light' when transparentBackground=1

An interim fix (hosting the iframe on a light card) made it readable but never themed: a light form on a black page. It is preserved as a [cut] note in docs/site-copy.md.

What this does

Renders the form as our own markup and POSTs it 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; the markup is ours, so it themes with the rest of the site.

Verified end to end

PASS  empty submit sends nothing (requests=0)
PASS  all 4 validation errors shown
PASS  focus moved to first invalid field
PASS  aria-invalid set + error linked via aria-describedby
PASS  Tally accepted the submission  200 {"submissionId":"Vpq0ZLN"}
PASS  routed to /thank-you, stayed on our own domain

Checked at 390 / 820 / 1440px in both themes. typecheck, lint and build all clean.

Notes for review

  • TALLY_FIELDS / TALLY_ROLE_OPTIONS are Tally's internal block UUIDs and are the contract with Tally's columns. If the form is edited in the Tally dashboard, they must be re-read or answers land in the wrong column. src/config.ts documents how.
  • The endpoint is undocumented. It could change without notice, so failure is handled loudly rather than silently: a non-2xx keeps everything the visitor typed, shows the error, and offers a mailto: fallback.
  • Success routes to /thank-you client-side, bypassing Tally's absolute production redirect (which previously sent people to the live site even from localhost).
  • Spam is handled by an off-screen honeypot, since the embed's own heuristics are gone.
  • Accessibility: bound labels, aria-invalid / aria-describedby per field, focus to the first invalid field, submit error via role="alert", required marker reads as "Required". 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.
  • 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.
  • Adds an optional Docker dev setup (compose.yaml, stock oven/bun:1, no Dockerfile), which is how this was validated.

Follow-up worth considering

There is no test runner in this repo, so the verification above was run as an ad-hoc Playwright script rather than committed. If you want that permanently guarded, it needs a test setup decision first.

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

@qxZap is attempting to deploy a commit to the Wandercode Team on Vercel.

A member of the Team first needs to authorize it.

@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 8:49am UTC

@cmin764

cmin764 commented Sep 9, 2026

Copy link
Copy Markdown
Member

Superseded by #14. I don't have push access to your fork (qxZap/website), so I couldn't push the review fixes onto this branch directly. #14 carries this exact commit unchanged plus three fixes from review (submit timeout, a duplicate CSS rule, dead .dockerignore, and stale doc references) — thanks for the original work here, the theming approach and the iframe writeup are exactly right.

Closing this one in favor of #14.

@cmin764 cmin764 closed this Sep 9, 2026
cmin764 added a commit that referenced this pull request Sep 9, 2026
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`).

---------

Co-authored-by: qxZap <MileaMihai@outlook.it>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 12cb33f4 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