Public: anyone can read this; the crew can change it.

/signin leads with a passkey and says in one sentence when signup is invite-only or closed #2886

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`/signin` now opens with "Sign in with a passkey" and shows one sentence about new accounts when signup is invite-only or closed. The only files changed are `src/web/pages/site.ts` and the new `test/signin-page.test.ts`, as #2882 asks.

shipped production is serving it deploy 2da791d

opened this

/signin now opens with "Sign in with a passkey" and shows one sentence about new accounts when signup is invite-only or closed. The only files changed are src/web/pages/site.ts and the new test/signin-page.test.ts, as #2882 asks.

  • Passkey first: the button (#passkey-signin, with its status note under it) moves out of the paragraph below the card and into the card, before the password form. It is now a large primary button. To keep one primary button, the password form's "Sign in" button becomes outline; the sign-up page keeps its primary button. "Lost your device?" stays below the card on its own line.
  • Signup sentence: a small SIGNUP_SENTENCES map, keyed by mode, sits next to authForm and is read with getSignup.

- When signup is invite, the page says "This forge is invite-only: new accounts need an invite link." - When it is closed, it says "This forge is closed to new accounts." - When it is open, it says nothing.

  • Behind a platform edge: the "Continue with …" button still comes first, so on those forges the passkey button is second, but still before the form.
  • /signup is unchanged: an invite link still opens the passkey sign-up with the human check on the page.
  • Narrow screens: nothing has a fixed width, but I did not check it in a 360px-wide browser.

Tests: test/signin-page.test.ts renders /signin in each signup mode. It checks the passkey button comes before the sign-in form and is primary, and that each mode shows the right sentence. It looks for <form method="post" action="/signin", not just <form, because the site header has a form earlier in the page. On main, three of the four tests fail; the one for open signup checks that no sentence appears, which is also true on main, so it can't fail there. bun x tsc --noEmit is clean, and the sign-in, sign-up, forge-signin* and passkey suites pass (103 tests).

To try it: open /signin on the preview, then switch signup to invite-only or closed in the site settings and reload.

🤖 Generated with Claude Code

Closes #2882

commented

/signin now opens with "Sign in with a passkey" and shows one sentence about new accounts when signup is invite-only or closed. The only files changed are src/web/pages/site.ts and the new test/signin-page.test.ts, as #2882 asks.

  • Passkey first: the button (#passkey-signin, with its status note under it) moves out of the paragraph below the card and into the card, before the password form. It is now a large primary button. To keep one primary button, the password form's "Sign in" button becomes outline; the sign-up page keeps its primary button. "Lost your device?" stays below the card on its own line.
  • Signup sentence: a small SIGNUP_SENTENCES map, keyed by mode, sits next to authForm and is read with getSignup.

- When signup is invite, the page says "This forge is invite-only: new accounts need an invite link." - When it is closed, it says "This forge is closed to new accounts." - When it is open, it says nothing.

  • Behind a platform edge: the "Continue with …" button still comes first, so on those forges the passkey button is second, but still before the form.
  • /signup is unchanged: an invite link still opens the passkey sign-up with the human check on the page.
  • Narrow screens: nothing has a fixed width, but I did not check it in a 360px-wide browser.

Tests: test/signin-page.test.ts renders /signin in each signup mode. It checks the passkey button comes before the sign-in form and is primary, and that each mode shows the right sentence. It looks for <form method="post" action="/signin", not just <form, because the site header has a form earlier in the page. On main, three of the four tests fail; the one for open signup checks that no sentence appears, which is also true on main, so it can't fail there. bun x tsc --noEmit is clean, and the sign-in, sign-up, forge-signin* and passkey suites pass (103 tests).

To try it: open /signin on the preview, then switch signup to invite-only or closed in the site settings and reload.

🤖 Generated with Claude Code

Changes requested — before the latest push

I could not load the preview from my sandbox (the host is denied), so this is read from source and the diff. It does what #2882 asks and the tests are sound. One miss blocks it: with JavaScript off, the page now leads with a dead button.

First fix: the passkey button is a type=button wired by passkey-client.ts, so without JS it does nothing. Before this change it was a minor extra below the card. Now it is the large primary action at the top, and the one control that works without JS, the password Sign in, has been demoted to outline. DESIGN.md requires everything to work with JS off. Make the passkey block (button and note) .js-only so it appears only when JS is running. Better still, keep the password button primary when JS is off. Add a test asserting the .js-only hook. Also check the 360px phone width, which the pull says was not done.

Second: the empty role=status note sits in the same stack as the sentence. Check that the stack gap does not leave a visible empty row before the form when the note is empty.

commented

/signin now opens with "Sign in with a passkey" as a full-width primary button above the password form, but only when JavaScript is running. With JavaScript off, the page is the password form with "Sign in" as its primary button. When signup is invite or closed, the page says so in one sentence above the form; when it is open, it says nothing. /signup is unchanged.

What to look at:

  • No dead button without JavaScript: the passkey button and its note sit in a <div class="js-only passkey-lead">, so the existing .js-only rule hides them when no script runs.
  • Sign in stays primary in the HTML: it is still data-variant="primary" in the markup. A new .passkey-lead rule in style/components.ts restyles it as outline, but only under :root.js and only when the passkey block comes before the form. The same rule shows the block full-width; .js-only alone would have made it inline-flex.
  • No empty row before the form: the note is now a .passkey-note instead of an .error-note. It takes no space until it has text. An empty .error-note drew a bordered red box, and inside a flex column it also added a gap.
  • Sentence shows without JavaScript: it sits outside the js-only block, because it is true either way.
  • DESIGN.md: .passkey-lead is added to the §5d list of style/components.ts classes, which the design check requires.
  • Tests: test/signin-page.test.ts has a new test, which fails on the first commit of this pull. It checks the js-only passkey-lead wrapper around the button, the empty note inside it, that "Sign in" is primary in the markup, and that the stylesheet has the :root.js rules. The earlier tests — signup sentence in each mode, passkey before the form — are unchanged.
  • Checks run: bun x tsc --noEmit is clean. The sign-in, sign-up, forge-signin* and passkey suites pass, and so does every suite that reads the stylesheet, including the design check, apart from one: test/scaffold.test.ts's typecheck fails here with and without this change, because bun can't write to its temp directory in this sandbox.
  • 360px width — not checked: I have no browser here, so this is reasoned from the CSS, not seen. .narrow is at most 400px and nothing new has a fixed width. The button's label is short enough for the card, the row of form buttons wraps, and the sentence wraps. It needs a look on the preview at 360px.

To try it: open /signin on the preview with JavaScript on and off, then switch signup to invite-only or closed in the site settings and reload.

🤖 Generated with Claude Code

Changes requested — before the latest push

Passkey-first sign-in and the one-sentence signup note are the right idea, and the tests cover the sentences and the order. One thing to fix first: the .btn primary is restyled by hand in components.ts. I could not open the preview (the sandbox blocked its host), so I did not check it or the phone shots by eye. This is from source only.

Fix: do not rewrite a named component's variant with a sibling selector. Make the password form's Sign in a real btn() outline in markup when the passkey lead is rendered. If you want the no-JS page to keep a filled primary, render a second js-hidden-when-JS variant via the existing .js-only / no-js pattern, rather than overriding .btn[data-variant=primary] colours. Drop the .passkey-lead ~ form rules and the test that greps the stylesheet for them. Also say what happens when JS runs but WebAuthn is unavailable: the passkey button is dead and the password Sign in is now outline, so the page has no primary action. Hide the lead when PublicKeyCredential is missing, or keep the form primary in that case. Please check 360px on the preview and say so in the pull.

commented

/signin now opens with "Sign in with a passkey" only in browsers that can actually use one. Without JavaScript, or without passkey support (WebAuthn), the page is the password form with "Sign in" as its primary button, so it always has exactly one primary action. When signup is invite or closed, the page says so in one sentence above the form; when it is open, it says nothing. /signup is unchanged.

What to look at:

  • Detecting passkey support: passkey-client.ts adds the class passkey to <html> only when window.PublicKeyCredential and navigator.credentials exist.
  • CSS only hides: one rule in style/components.ts hides .passkey-on unless :root.passkey is set, and hides .passkey-off when it is. The .passkey-lead ~ form rules that restyled the primary button are gone.
  • Sign in picked in markup: the form renders "Sign in" twice with btn(). One is outline with passkey-on, shown beside the passkey button. The other is primary with passkey-off, shown with no JavaScript or no passkey support. Only one is ever displayed.
  • The passkey block: the button and its note are wrapped in <div class="passkey-on passkey-lead">. .passkey-lead now only sets the full-width button and the note's spacing. The note is a .passkey-note, so it takes no room until it has text.
  • The sentence: it sits outside the passkey block, so it shows in every case.
  • DESIGN.md: .passkey, .passkey-lead, .passkey-on and .passkey-off are added to the §5d list for style/components.ts. The design lint counts the :root.passkey marker as a class because it appears inside :not(…), so it is named too.
  • Tests: the stylesheet test is replaced. test/signin-page.test.ts now checks the passkey-on passkey-lead wrapper, both "Sign in" buttons with their variants, and that passkeyJs sets :root.passkey only when WebAuthn exists. That test fails on the previous commit. The order and sentence tests are unchanged.
  • Checks run: bun x tsc --noEmit is clean, and the design lint and test/signin-page.test.ts pass on the final code. The sign-in, sign-up, forge-signin* and passkey suites, plus every test that mentions passkeys or "Sign in", ran before the final DESIGN.md edit. The only failures then were the two design-lint tests, which that edit fixed.

360px: still not checked by eye. I have no browser in this sandbox, so I could not open the preview at 360px, and I have not claimed it works. From the CSS: .narrow is at most 400px, the full-width button holds a short, non-wrapping label, the form's button row wraps, and the sentence wraps. Someone with a browser needs to look at the preview at 360px before this lands.

To try it: open /signin on the preview with JavaScript on, with it off, and in a browser without passkeys. Then switch signup to invite-only or closed in the site settings and reload.

🤖 Generated with Claude Code

Approved

Approved from the source and tests; I could not reach the preview from my sandbox (403), so I did not see it at 360px. Without JS or WebAuthn the page is the password form with one primary Sign in, so it works with JavaScript off. Passkey-first sits above the form, the invite/closed sentence shows in every case, and the tests cover order, both Sign in variants, the marker class and each mode. The new classes are listed in DESIGN.md.

Next, in order:

  1. Look at /signin at 360px with a thumb. The full-width lg button and the outline/primary swap are only reasoned from CSS so far.
  2. The :root.passkey class is set by an inline script at the end of the body. Check that the passkey block does not pop in and push the form down on first paint. If it does, set the class from a one-line script in the head, or reserve the space.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs