From ba681677043420570557b2221192223a480f8cbd Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Tue, 13 Oct 2026 15:59:35 +0200 Subject: [PATCH] Greet with the Feuerwerk logo on the sign-in screen The sign-in screen was a plain box with the name as a title. It now shows the brand, so the first thing anyone sees is Feuerwerk itself. - The stacked logo (name below the icon) sits above the card. It is drawn as vectors straight from public/brand/, so it stays sharp at any zoom. Its is stripped first, Pixi warns about it. - The card is a floating panel, 300 px wide, with more padding and a thin strip in the three colours of the logo along its top. - The small "Feuerwerk" title in the card is gone, the logo says it. - Logo and card sit a little above the middle of the screen. - Labels, field names and error texts are unchanged, the tests look for them. Tests: - Gate on melektaus-atx: tsc, eslint, Prettier clean, 307 files, 4762 tests green - e2e 45 (login): ALL PASSED, 19 checks, run while the screen still had a glow behind the logo. The glow was taken out by hand after that, e2e 45 did not run again. - Looked at in Chromium at 1280x800, empty and with a wrong password Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- src/editor/auth/LoginScreen.ts | 79 ++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/src/editor/auth/LoginScreen.ts b/src/editor/auth/LoginScreen.ts index f90f5a7b..51158df2 100644 --- a/src/editor/auth/LoginScreen.ts +++ b/src/editor/auth/LoginScreen.ts @@ -1,24 +1,73 @@ import { Graphics } from 'pixi.js'; +import logoSvg from '../../../public/brand/feuerwerk-logo-stacked-dark.svg?raw'; import { type AuthStore, type SignedIn, WrongCredentialsError } from '../../core/store/AuthStore'; -import { Button, drawControlBox, Label, Spacer, TextField, Theme, ui, VStack } from '../../ui'; +import { + Button, + drawFloatingBox, + drawSvg, + Label, + Spacer, + TextField, + Theme, + ui, + VStack, + Widget, +} from '../../ui'; import { FormStatus, refocus } from './FormStatus'; import { loginError, loginProblem } from './loginRules'; -const CARD_WIDTH = 280; +const CARD_WIDTH = 300; +const CARD_PADDING = Theme.padding * 3; +const FIELD_WIDTH = CARD_WIDTH - CARD_PADDING * 2; -/** The box in the middle of the screen; the screen around it is the plain editor background. */ +/** The three colours of the logo's rays, outside in: corners, spokes, diagonals. */ +const BRAND_COLORS = [0x3fb6a8, 0xf5b83d, 0xf0782f] as const; + +/** Where the stacked logo draws in its own units: its `viewBox`. */ +const LOGO_BOX = { x: -62.876, y: 0, w: 247.962, h: 175.806 }; +const LOGO_SCALE = 0.8; + +/** The logo with the name below it (`public/brand/`), drawn as vectors so it stays sharp at any zoom. */ +class Logo extends Widget { + private readonly picture = new Graphics(); + + constructor() { + super(); + this.label = 'login-logo'; + this.eventMode = 'none'; + this.minWidth = Math.round(LOGO_BOX.w * LOGO_SCALE); + this.minHeight = Math.round(LOGO_BOX.h * LOGO_SCALE); + // The file's `<title>` is for screen readers; Pixi's parser draws nothing for it but warns. + drawSvg(this.picture, logoSvg.replace(/<title>[^<]*<\/title>/, ''), Theme.colors.text); + this.picture.scale.set(LOGO_SCALE); + this.addChild(this.picture); + } + + override layout(): void { + const x = (this.w - LOGO_BOX.w * LOGO_SCALE) / 2 - LOGO_BOX.x * LOGO_SCALE; + const y = (this.h - LOGO_BOX.h * LOGO_SCALE) / 2 - LOGO_BOX.y * LOGO_SCALE; + this.picture.position.set(Math.round(x), Math.round(y)); + } +} + +/** The box under the logo: a floating panel with a strip in the logo's colours along its top. */ class Card extends VStack { private readonly bg = new Graphics(); constructor() { - super({ padding: Theme.padding * 2, gap: Theme.gap }); + super({ padding: CARD_PADDING, gap: Theme.gap }); this.addChildAt(this.bg, 0); this.minWidth = CARD_WIDTH; } override redraw(): void { - this.bg.clear(); - drawControlBox(this.bg, this.w, this.h, { fill: Theme.colors.panel, border: Theme.colors.border }); + drawFloatingBox(this.bg, this.w, this.h, Theme.colors.panel); + const strip = 3; + const third = this.w / BRAND_COLORS.length; + BRAND_COLORS.forEach((color, i) => { + this.bg.rect(Math.round(i * third), 0, Math.round((i + 1) * third) - Math.round(i * third), strip); + this.bg.fill(color); + }); } } @@ -30,6 +79,7 @@ class Card extends VStack { * signed in, so there is nothing behind it to hide. */ export class LoginScreen extends VStack { + private readonly logo = new Logo(); private readonly user: TextField; private readonly password: TextField; private readonly submit: Button; @@ -44,23 +94,25 @@ export class LoginScreen extends VStack { ) { super({ align: 'center', gap: Theme.gap }); this.add(new Spacer(), 1); + this.add(this.logo); + this.add(new Spacer(Theme.padding * 3)); const card = new Card(); - card.add(new Label('Feuerwerk', { style: 'title' })); - card.add(new Label('Sign in to open your projects', { style: 'muted' })); - card.add(new Spacer(Theme.gap)); + card.add(new Label('Sign in to open your projects', { style: 'bodyBold' })); + card.add(new Spacer(Theme.padding)); card.add(new Label('User name', { style: 'small' })); this.user = new TextField('', { - width: CARD_WIDTH - Theme.padding * 4, + width: FIELD_WIDTH, placeholder: 'User name', onSubmit: () => ui().focus.focus(this.password), }); card.add(this.user); + card.add(new Spacer(Theme.gap)); card.add(new Label('Password', { style: 'small' })); this.password = new TextField('', { - width: CARD_WIDTH - Theme.padding * 4, + width: FIELD_WIDTH, placeholder: 'Password', secret: true, onSubmit: () => void this.signIn(), @@ -70,13 +122,14 @@ export class LoginScreen extends VStack { card.add(this.status.message); this.submit = new Button('Sign in', { variant: 'primary', - width: CARD_WIDTH - Theme.padding * 4, + width: FIELD_WIDTH, onClick: () => void this.signIn(), }); card.add(this.submit); this.add(card); - this.add(new Spacer(), 1); + // Twice the space below as above, so logo and card sit a little above the middle. + this.add(new Spacer(), 2); } /** Puts the caret in the first field, so a keyboard alone can sign in. */