From 4ed36e0d3859cc76b6d38104709696831d0c13bd Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Thu, 17 Sep 2026 22:54:00 +0300 Subject: [PATCH 1/3] fix(auth): give the sign-in input a text colour, and restore the theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The email field computed its own background one step lighter than the page and never set `color`. Form controls do not inherit it, so the text someone typed was the UA default — black, over a near-black field. There was no `color-scheme` either, so the browser rendered the control in light appearance to begin with. The theme was a second, separate loss: `issuer()` still takes one and calls `setTheme`, but nothing had passed one since `packages/auth` became a vendored fork, so every sign-in rendered as OpenAuth — its font, its periwinkle, its logo. Restored from the pre-fork config, with the logo and favicon repointed because both URLs it carried now 404 and a broken `logo` renders a broken image rather than falling back. --- apps/auth/src/index.ts | 34 ++++++++++++++++++++++++++++++++++ packages/auth/src/ui/css.ts | 23 +++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/apps/auth/src/index.ts b/apps/auth/src/index.ts index 045be5d0..881def5b 100644 --- a/apps/auth/src/index.ts +++ b/apps/auth/src/index.ts @@ -2,6 +2,7 @@ import type { Hyperdrive } from '@cloudflare/workers-types'; import { issuer } from '@nestri/auth/index'; import { CodeProvider } from '@nestri/auth/provider/code'; import { CodeUI } from '@nestri/auth/ui/code'; +import type { Theme } from '@nestri/auth/ui/theme'; import { isDomainMatch } from '@nestri/auth/util'; import { Actor } from '@nestri/core/actor'; import { PostgresCodeStore } from '@nestri/core/auth/authorization-code'; @@ -169,11 +170,44 @@ export const allowClient = async ( return isDomainMatch(redirect, host); }; +/** + * The sign-in screen's theme. + * + * Restored from the pre-fork issuer config, which carried these exact values + * before the vendored `packages/auth` replaced it and nothing set a theme at + * all — leaving every sign-in on `THEME_OPENAUTH`, which is somebody else's + * brand and, with the input bug this shipped beside, an unreadable one. + * + * `primary` is the one value worth not changing casually: it is the brand + * orange, and the button's own text colour is derived from its lightness + * rather than stated, so a lighter primary silently flips that text to black. + */ +const THEME_NESTRI: Theme = { + title: 'Nestri | Auth', + primary: '#FF4F01', + // Not the URLs the old config carried: `/logo.webp` and `/seo/favicon.ico` + // both 404 today, and `base.tsx` falls back to OpenAuth's own mark only when + // `logo` is absent — a broken URL renders a broken image instead. These two + // are what the site actually serves. + logo: 'https://nestri.io/images/android-chrome-512x512.png', + favicon: 'https://nestri.io/images/favicon.ico', + background: { + light: '#f5f5f5', + dark: '#171717' + }, + radius: 'lg', + font: { + family: 'Geist, sans-serif' + }, + css: `@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');` +}; + export default { async fetch(request: Request, env: Env, ctx?: ExecutionContext) { Env.init(env as unknown as Record); const inner = issuer({ subjects, + theme: THEME_NESTRI, // One database behind all of it, and nothing that only exists on // one hosting provider. What is left in the generic store is the // rate-limit counters — the only records here that are allowed to diff --git a/packages/auth/src/ui/css.ts b/packages/auth/src/ui/css.ts index 4d88b83a..0c439ee0 100644 --- a/packages/auth/src/ui/css.ts +++ b/packages/auth/src/ui/css.ts @@ -1,4 +1,6 @@ export default `:root { + color-scheme: light dark; + --color-background-dark: #0e0e11; --color-background-light: #ffffff; --color-primary-dark: #6772e5; @@ -133,6 +135,11 @@ export default `:root { h ); background: var(--background); + /* Form controls do not inherit \`color\`, so without this the text someone + types is the UA default — black, over the near-black \`--background\` + computed just above. The page looked fine and the field was unreadable. */ + color: var(--color-high); + caret-color: var(--color-high); border-color: oklch( from var(--color-background) calc(clamp(0.22, l + (-0.12 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.06), 0.88)) c h @@ -141,6 +148,22 @@ export default `:root { font-size: var(--font-size-sm); outline: none; + &::placeholder { + color: var(--color-high); + opacity: 0.5; + } + + /* Chrome paints its own background over autofilled fields and ignores + \`background\`. An inset shadow is the only thing it honours, and + \`-webkit-text-fill-color\` the only thing that moves the glyphs. */ + &:-webkit-autofill, + &:-webkit-autofill:hover, + &:-webkit-autofill:focus { + -webkit-text-fill-color: var(--color-high); + -webkit-box-shadow: 0 0 0 100px var(--background) inset; + caret-color: var(--color-high); + } + &:focus { border-color: oklch( from var(--color-background) From 74391714d0f6c74fea4038f6e5219520573a2a2c Mon Sep 17 00:00:00 2001 From: Wanjohi Date: Thu, 17 Sep 2026 23:05:39 +0300 Subject: [PATCH 2/3] feat(auth): draw the sign-in screen in the product's design language The screen was still the upstream template's: its font, its accent, its logo, and a theme that tried to serve a light and a dark scheme from one set of colours by deriving each one from the background's lightness. That derivation is replaced with stated values, and the page is dark only. Black, a neutral grey ramp, one brand accent, Mona Sans for the display line and Geist for anything read or typed; two dashed bands closing into a box on a wide screen, a dashed vertical either side of the column, the wordmark, and the line of copy the rest of the product opens with. The email field keeps the fix from the previous commit and takes the brand colour on focus, which is the only place it appears besides the wordmark. Measured against a render of the same design built from its own source: the band rules, the column rules, the wordmark box and the button height land on identical pixels. --- apps/auth/src/index.ts | 41 +-- packages/auth/src/ui/base.tsx | 211 ++++++++---- packages/auth/src/ui/code.tsx | 31 +- packages/auth/src/ui/css.ts | 595 ++++++++++++++++++++++------------ 4 files changed, 564 insertions(+), 314 deletions(-) diff --git a/apps/auth/src/index.ts b/apps/auth/src/index.ts index 881def5b..d75d5525 100644 --- a/apps/auth/src/index.ts +++ b/apps/auth/src/index.ts @@ -173,33 +173,27 @@ export const allowClient = async ( /** * The sign-in screen's theme. * - * Restored from the pre-fork issuer config, which carried these exact values - * before the vendored `packages/auth` replaced it and nothing set a theme at - * all — leaving every sign-in on `THEME_OPENAUTH`, which is somebody else's - * brand and, with the input bug this shipped beside, an unreadable one. + * Almost everything that used to live here is now stated in + * `packages/auth/src/ui/css.ts`, which states the product's design language + * longhand. What is left here is the handful of values the + * issuer itself needs — and `primary`, which is the one colour the stylesheet + * reads back from the theme so the brand has a single source. * - * `primary` is the one value worth not changing casually: it is the brand - * orange, and the button's own text colour is derived from its lightness - * rather than stated, so a lighter primary silently flips that text to black. + * There is no `background` and no light variant on purpose: the page is dark + * only, and the derived-colour scheme that made two schemes possible is + * exactly what rendered the sign-in field's text the colour of its own + * background. */ const THEME_NESTRI: Theme = { - title: 'Nestri | Auth', - primary: '#FF4F01', - // Not the URLs the old config carried: `/logo.webp` and `/seo/favicon.ico` - // both 404 today, and `base.tsx` falls back to OpenAuth's own mark only when - // `logo` is absent — a broken URL renders a broken image instead. These two - // are what the site actually serves. - logo: 'https://nestri.io/images/android-chrome-512x512.png', + title: 'Login | Nestri', + primary: 'hsl(12 84% 53%)', favicon: 'https://nestri.io/images/favicon.ico', - background: { - light: '#f5f5f5', - dark: '#171717' - }, - radius: 'lg', - font: { - family: 'Geist, sans-serif' - }, - css: `@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap');` + // Mona Sans for the display line and the action, Geist for everything a + // person reads or types. Served from the Fontsource CDN because the + // self-hosted font packages need a bundler and this page is a string + // rendered in a Worker with no build step. The family names must match the + // ones the stylesheet asks for. + css: `@import url('https://cdn.jsdelivr.net/fontsource/css/mona-sans:vf@latest/wght.css');@import url('https://cdn.jsdelivr.net/fontsource/css/geist:vf@latest/wght.css');` }; export default { @@ -255,7 +249,6 @@ export default { // nothing — and a mistyped address that silently succeeds // leaves someone waiting for mail that went nowhere. ...CodeUI({ - copy: { code_info: "We'll email you a code to sign in." }, sendCode: async () => {} }), sendCode: async (claims, code) => { diff --git a/packages/auth/src/ui/base.tsx b/packages/auth/src/ui/base.tsx index d61424f6..2b312d15 100644 --- a/packages/auth/src/ui/base.tsx +++ b/packages/auth/src/ui/base.tsx @@ -1,15 +1,30 @@ +/** @jsxImportSource hono/jsx */ + import { PropsWithChildren } from 'hono/jsx'; +import css from './css.js'; import { getTheme } from './theme.js'; -import css from './css.js'; - +/** + * The page every sign-in screen is drawn inside. + * + * Two dashed bands across the top and bottom, closing into a 1440px-wide box + * on a wide screen; between them a 48-column field with a dashed vertical on + * the fifth gridline from each edge; and centred in it the lockup — wordmark, + * one line of copy, then whatever the provider is asking for. + * + * Dark only. There is no light variant to get wrong, which is the point: + * the previous version derived its colours from the background so that one + * theme could serve both, and that derivation is what left the input's text + * the same colour as the input's background. + */ export function Layout( props: PropsWithChildren<{ size?: 'small'; }> ) { const theme = getTheme(); + function get(key: 'primary' | 'background' | 'logo', mode: 'light' | 'dark') { if (!theme) return; if (!theme[key]) return; @@ -18,69 +33,59 @@ export function Layout( return theme[key][mode] as string | undefined; } - const radius = (() => { - if (theme?.radius === 'none') return '0'; - if (theme?.radius === 'sm') return '1'; - if (theme?.radius === 'md') return '1.25'; - if (theme?.radius === 'lg') return '1.5'; - if (theme?.radius === 'full') return '1000000000001'; - return '1'; - })(); - - const hasLogo = get('logo', 'light') && get('logo', 'dark'); + // The one value the theme still drives. Everything else is stated in the + // stylesheet, because a second place to set a colour is a second place for + // it to be wrong. + const brand = get('primary', 'dark') ?? get('primary', 'light'); return ( - + - {theme?.title || 'OpenAuthJS'} + {theme?.title || 'Nestri'} - {theme?.favicon ? ( - - ) : ( - <> - - - - - - )} + + + {theme?.favicon && }