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 <title> 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>
This commit is contained in:
1 parent
d4ff652293
commit
ba68167704
1 file changed
+66
-13
@@ -1,24 +1,73 @@
|
|||||||
import { Graphics } from 'pixi.js';
|
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 { 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 { FormStatus, refocus } from './FormStatus';
|
||||||
import { loginError, loginProblem } from './loginRules';
|
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 {
|
class Card extends VStack {
|
||||||
private readonly bg = new Graphics();
|
private readonly bg = new Graphics();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ padding: Theme.padding * 2, gap: Theme.gap });
|
super({ padding: CARD_PADDING, gap: Theme.gap });
|
||||||
this.addChildAt(this.bg, 0);
|
this.addChildAt(this.bg, 0);
|
||||||
this.minWidth = CARD_WIDTH;
|
this.minWidth = CARD_WIDTH;
|
||||||
}
|
}
|
||||||
|
|
||||||
override redraw(): void {
|
override redraw(): void {
|
||||||
this.bg.clear();
|
drawFloatingBox(this.bg, this.w, this.h, Theme.colors.panel);
|
||||||
drawControlBox(this.bg, this.w, this.h, { fill: Theme.colors.panel, border: Theme.colors.border });
|
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.
|
* signed in, so there is nothing behind it to hide.
|
||||||
*/
|
*/
|
||||||
export class LoginScreen extends VStack {
|
export class LoginScreen extends VStack {
|
||||||
|
private readonly logo = new Logo();
|
||||||
private readonly user: TextField;
|
private readonly user: TextField;
|
||||||
private readonly password: TextField;
|
private readonly password: TextField;
|
||||||
private readonly submit: Button;
|
private readonly submit: Button;
|
||||||
@@ -44,23 +94,25 @@ export class LoginScreen extends VStack {
|
|||||||
) {
|
) {
|
||||||
super({ align: 'center', gap: Theme.gap });
|
super({ align: 'center', gap: Theme.gap });
|
||||||
this.add(new Spacer(), 1);
|
this.add(new Spacer(), 1);
|
||||||
|
this.add(this.logo);
|
||||||
|
this.add(new Spacer(Theme.padding * 3));
|
||||||
|
|
||||||
const card = new Card();
|
const card = new Card();
|
||||||
card.add(new Label('Feuerwerk', { style: 'title' }));
|
card.add(new Label('Sign in to open your projects', { style: 'bodyBold' }));
|
||||||
card.add(new Label('Sign in to open your projects', { style: 'muted' }));
|
card.add(new Spacer(Theme.padding));
|
||||||
card.add(new Spacer(Theme.gap));
|
|
||||||
|
|
||||||
card.add(new Label('User name', { style: 'small' }));
|
card.add(new Label('User name', { style: 'small' }));
|
||||||
this.user = new TextField('', {
|
this.user = new TextField('', {
|
||||||
width: CARD_WIDTH - Theme.padding * 4,
|
width: FIELD_WIDTH,
|
||||||
placeholder: 'User name',
|
placeholder: 'User name',
|
||||||
onSubmit: () => ui().focus.focus(this.password),
|
onSubmit: () => ui().focus.focus(this.password),
|
||||||
});
|
});
|
||||||
card.add(this.user);
|
card.add(this.user);
|
||||||
|
card.add(new Spacer(Theme.gap));
|
||||||
|
|
||||||
card.add(new Label('Password', { style: 'small' }));
|
card.add(new Label('Password', { style: 'small' }));
|
||||||
this.password = new TextField('', {
|
this.password = new TextField('', {
|
||||||
width: CARD_WIDTH - Theme.padding * 4,
|
width: FIELD_WIDTH,
|
||||||
placeholder: 'Password',
|
placeholder: 'Password',
|
||||||
secret: true,
|
secret: true,
|
||||||
onSubmit: () => void this.signIn(),
|
onSubmit: () => void this.signIn(),
|
||||||
@@ -70,13 +122,14 @@ export class LoginScreen extends VStack {
|
|||||||
card.add(this.status.message);
|
card.add(this.status.message);
|
||||||
this.submit = new Button('Sign in', {
|
this.submit = new Button('Sign in', {
|
||||||
variant: 'primary',
|
variant: 'primary',
|
||||||
width: CARD_WIDTH - Theme.padding * 4,
|
width: FIELD_WIDTH,
|
||||||
onClick: () => void this.signIn(),
|
onClick: () => void this.signIn(),
|
||||||
});
|
});
|
||||||
card.add(this.submit);
|
card.add(this.submit);
|
||||||
this.add(card);
|
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. */
|
/** Puts the caret in the first field, so a keyboard alone can sign in. */
|
||||||
|
|||||||
Reference in new issue
Block a user