From 663b2c424f2c9985f7f3945e82da7116d282799a Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Fri, 26 Jun 2026 14:37:01 +0700 Subject: [PATCH 1/7] Refresh sign-in / register / forgot-password pages with dark theme Drop the white auth-container "platter" and apply shadcn-style theme aliases (--background, --foreground, --border, --muted-foreground, --ring) on the wrapper so BoxelInput and the secondary-dark Button kind re-theme automatically across login, register, and forgot-password. Add a multi-color Google G icon to the Sign-in-with-Google CTA, switch both secondary CTAs to secondary-dark with a contrast nudge so the dark buttons sit a hair lighter than the page, remove the "or" dividers, and move the Boxel logo to the upper-left of the viewport. No change to the SSO flow, the GOOGLE_AUTH_ENABLED gate, or the password login task. Refs: CS-11712 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../boxel-ui/addon/raw-icons/google-color.svg | 6 ++ packages/boxel-ui/addon/src/icons.gts | 3 + .../boxel-ui/addon/src/icons/google-color.gts | 30 ++++++++ .../app/components/matrix/auth-container.gts | 69 +++++++++++-------- .../app/components/matrix/forgot-password.gts | 21 ++++-- packages/host/app/components/matrix/login.gts | 37 +++++++--- .../app/components/matrix/register-user.gts | 22 ++++-- 7 files changed, 136 insertions(+), 52 deletions(-) create mode 100644 packages/boxel-ui/addon/raw-icons/google-color.svg create mode 100644 packages/boxel-ui/addon/src/icons/google-color.gts diff --git a/packages/boxel-ui/addon/raw-icons/google-color.svg b/packages/boxel-ui/addon/raw-icons/google-color.svg new file mode 100644 index 00000000000..ce52e793d31 --- /dev/null +++ b/packages/boxel-ui/addon/raw-icons/google-color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/boxel-ui/addon/src/icons.gts b/packages/boxel-ui/addon/src/icons.gts index b3c82ba6da7..7c86c31da58 100644 --- a/packages/boxel-ui/addon/src/icons.gts +++ b/packages/boxel-ui/addon/src/icons.gts @@ -36,6 +36,7 @@ import Fitted from './icons/fitted.gts'; import Folder from './icons/folder.gts'; import Form from './icons/form.gts'; import FourLines from './icons/four-lines.gts'; +import GoogleColor from './icons/google-color.gts'; import Grid3x3 from './icons/grid-3x3.gts'; import Group from './icons/group.gts'; import Head from './icons/head.gts'; @@ -116,6 +117,7 @@ export const ALL_ICON_COMPONENTS = [ Folder, Form, FourLines, + GoogleColor, Grid3x3, Group, Head, @@ -198,6 +200,7 @@ export { Folder, Form, FourLines, + GoogleColor, Grid3x3, Group, Head, diff --git a/packages/boxel-ui/addon/src/icons/google-color.gts b/packages/boxel-ui/addon/src/icons/google-color.gts new file mode 100644 index 00000000000..2bf167f6083 --- /dev/null +++ b/packages/boxel-ui/addon/src/icons/google-color.gts @@ -0,0 +1,30 @@ +// This file is auto-generated by 'pnpm rebuild:icons' +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import type { Signature } from './types.ts'; + +const IconComponent: TemplateOnlyComponent = ; + +// @ts-expect-error this is the only way to set a name on a Template Only Component currently +IconComponent.name = 'GoogleColor'; +export default IconComponent; diff --git a/packages/host/app/components/matrix/auth-container.gts b/packages/host/app/components/matrix/auth-container.gts index 37abe7a243a..67e53653ed3 100644 --- a/packages/host/app/components/matrix/auth-container.gts +++ b/packages/host/app/components/matrix/auth-container.gts @@ -1,6 +1,5 @@ import type { TemplateOnlyComponent } from '@ember/component/template-only'; -import { CardContainer, BoxelHeader } from '@cardstack/boxel-ui/components'; import { BoxelIcon } from '@cardstack/boxel-ui/icons'; interface Signature { @@ -11,26 +10,50 @@ interface Signature { const AuthContainer: TemplateOnlyComponent = ; diff --git a/packages/host/app/components/matrix/forgot-password.gts b/packages/host/app/components/matrix/forgot-password.gts index 2097560f296..417893f17f1 100644 --- a/packages/host/app/components/matrix/forgot-password.gts +++ b/packages/host/app/components/matrix/forgot-password.gts @@ -72,10 +72,10 @@ export default class ForgotPassword extends Component { @loading={{this.sendEmailValidationTask.isRunning}} {{on 'click' this.sendEmailValidation}} >Reset Your Password - or @@ -171,6 +171,7 @@ export default class ForgotPassword extends Component { diff --git a/packages/host/app/components/matrix/login.gts b/packages/host/app/components/matrix/login.gts index 0be36a1a342..b7f7f339456 100644 --- a/packages/host/app/components/matrix/login.gts +++ b/packages/host/app/components/matrix/login.gts @@ -19,6 +19,7 @@ import { FieldContainer, BoxelInput, } from '@cardstack/boxel-ui/components'; +import { GoogleColor } from '@cardstack/boxel-ui/icons'; import { isMatrixError, @@ -100,19 +101,22 @@ export default class Login extends Component { {{#if this.error}}
{{this.error}}
{{/if}} - or {{#if this.showGoogleButton}} + > + + Sign in with Google + {{/if}} @@ -125,6 +129,7 @@ export default class Login extends Component { } .title { font: 600 var(--boxel-font-md); + color: var(--foreground); margin-bottom: var(--boxel-sp-sm); padding: 0; } @@ -141,7 +146,7 @@ export default class Login extends Component { padding: 0; margin-bottom: var(--boxel-sp-lg); margin-left: auto; - color: var(--boxel-dark); + color: var(--foreground); font: 500 var(--boxel-font-xs); } .forgot-password:hover { @@ -157,12 +162,24 @@ export default class Login extends Component { justify-content: center; align-items: center; } + .secondary-cta { + /* Sit a hair lighter than the page bg so the dark CTAs pop. */ + --boxel-button-color: var(--boxel-700); + margin-top: var(--boxel-sp-sm); + } + .secondary-cta:not(:disabled):hover, + .secondary-cta:not(:disabled):active { + --boxel-button-color: var(--boxel-600); + } .google-button { - margin-bottom: var(--boxel-sp-sm); + display: inline-flex; + align-items: center; + gap: var(--boxel-sp-xs); } - .or { - margin: var(--boxel-sp-sm) auto; - font: 500 var(--boxel-font-sm); + .google-g { + width: 1.125rem; + height: 1.125rem; + flex-shrink: 0; } .error { color: var(--boxel-error-100); diff --git a/packages/host/app/components/matrix/register-user.gts b/packages/host/app/components/matrix/register-user.gts index 3def39659d2..f40e36897d2 100644 --- a/packages/host/app/components/matrix/register-user.gts +++ b/packages/host/app/components/matrix/register-user.gts @@ -220,10 +220,10 @@ export default class RegisterUser extends Component { @loading={{this.doRegistrationFlow.isRunning}} {{on 'click' this.register}} >Create Account - or @@ -232,8 +232,12 @@ export default class RegisterUser extends Component { +; + +export default AuthButton; diff --git a/packages/host/app/components/matrix/auth-form-field.gts b/packages/host/app/components/matrix/auth-form-field.gts new file mode 100644 index 00000000000..3b5eb56ad41 --- /dev/null +++ b/packages/host/app/components/matrix/auth-form-field.gts @@ -0,0 +1,60 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import { FieldContainer } from '@cardstack/boxel-ui/components'; + +interface Signature { + Element: HTMLElement; + Args: { + label: string; + }; + Blocks: { default: [] }; +} + +const AuthFormField: TemplateOnlyComponent = ; + +export default AuthFormField; diff --git a/packages/host/app/components/matrix/forgot-password.gts b/packages/host/app/components/matrix/forgot-password.gts index 76536667a74..7ab1328fcd8 100644 --- a/packages/host/app/components/matrix/forgot-password.gts +++ b/packages/host/app/components/matrix/forgot-password.gts @@ -11,19 +11,19 @@ import perform from 'ember-concurrency/helpers/perform'; import { v4 as uuidv4 } from 'uuid'; -import { - Button, - FieldContainer, - BoxelInput, -} from '@cardstack/boxel-ui/components'; +import { BoxelInput } from '@cardstack/boxel-ui/components'; import { eq, or } from '@cardstack/boxel-ui/helpers'; import { isMatrixError, isValidPassword, } from '@cardstack/host/lib/matrix-utils'; + import type MatrixService from '@cardstack/host/services/matrix-service'; +import AuthButton from './auth-button'; +import AuthFormField from './auth-form-field'; + import type { AuthMode } from './auth'; export type ResetPasswordParams = { @@ -46,12 +46,7 @@ export default class ForgotPassword extends Component { }} Forgot your password?

Enter email to receive a reset password link.

- + { @value={{this.email}} @onInput={{this.setEmail}} /> - +
- - + >Back to login
{{else if (eq this.state.type 'waitForEmailValidation')}} Please check your email to @@ -87,22 +80,16 @@ export default class ForgotPassword extends Component {
  • We've sent an email to {{this.state.email}}
  • Click on the link within the email to reset your password
  • - + >Resend Email {{else if (eq this.state.type 'resetPassword')}} Reset your password - + { @onInput={{this.setPassword}} @onBlur={{this.checkPassword}} /> - - + + { @onInput={{this.setConfirmPassword}} @onBlur={{this.checkConfirmPassword}} /> - +
    - + >Reset Password
    {{#if this.isResetPasswordParamsError}} Unable to reset your @@ -159,12 +140,11 @@ export default class ForgotPassword extends Component {

    Your password has been successfully reset. You can use the link below to sign into your Boxel account with your new password.

    - + >Sign In to Boxel
    {{/if}} @@ -181,32 +161,6 @@ export default class ForgotPassword extends Component { line-height: 20px; color: var(--foreground); } - .field { - margin-top: var(--boxel-sp); - } - .field :deep(input:autofill) { - transition: - background-color 0s 600000s, - color 0s 600000s; - } - .field :deep(.validation-icon-container.invalid) { - display: none; - } - .field :deep(.boxel-input-group--invalid > :nth-last-child(2)) { - border-top-right-radius: var(--boxel-input-group-border-radius); - border-bottom-right-radius: var(--boxel-input-group-border-radius); - border-right-width: var(--boxel-input-group-interior-border-width); - } - .field - :deep( - .boxel-input-group:not(.boxel-input-group--invalid) - > :nth-last-child(2) - ) { - padding-right: 0; - } - .field :deep(.error-message) { - margin-left: 0; - } .button-wrapper { width: 100%; display: flex; @@ -216,38 +170,6 @@ export default class ForgotPassword extends Component { margin-top: var(--boxel-sp-lg); gap: var(--boxel-sp-sm); } - .button-wrapper button { - margin: 0; - width: 100%; - } - .button { - --boxel-button-padding: var(--boxel-sp-sm) var(--boxel-sp-lg); - width: fit-content; - min-width: 148px; - } - .kind-primary { - --boxel-button-color: var(--auth-primary-bg); - --boxel-button-text-color: var(--auth-primary-text); - } - .kind-primary:disabled { - --boxel-button-color: var(--auth-primary-disabled-bg); - --boxel-button-text-color: var(--auth-primary-disabled-text); - --boxel-button-border: none; - } - .secondary-cta { - --boxel-button-color: var(--auth-secondary-bg); - --boxel-button-text-color: var(--auth-secondary-text); - --boxel-button-border: 1px solid var(--auth-secondary-border); - } - .secondary-cta:not(:disabled):hover, - .secondary-cta:not(:disabled):active { - --boxel-button-color: var(--auth-secondary-hover-bg); - } - .button :deep(.boxel-loading-indicator) { - display: flex; - justify-content: center; - align-items: center; - } .email-validation-instruction { padding: 0; list-style-position: inside; @@ -262,7 +184,7 @@ export default class ForgotPassword extends Component { color: var(--boxel-error-100); padding: 0; font: 500 var(--boxel-font-xs); - margin: var(--boxel-sp-xxs) auto 0 auto; + margin: var(--boxel-sp-2xs) auto 0 auto; text-align: center; } .error a { @@ -309,18 +231,18 @@ export default class ForgotPassword extends Component { } private get isForgotPasswordBtnDisabled() { - return ( - !this.email || this.emailError || this.sendEmailValidationTask.isRunning + return Boolean( + !this.email || this.emailError || this.sendEmailValidationTask.isRunning, ); } private get isResetPasswordBtnDisabled() { - return ( + return Boolean( !this.password || !this.confirmPassword || this.passwordError || this.confirmPasswordError || - this.resetPassword.isRunning + this.resetPassword.isRunning, ); } diff --git a/packages/host/app/components/matrix/login.gts b/packages/host/app/components/matrix/login.gts index 4e2945947ec..da587a7e741 100644 --- a/packages/host/app/components/matrix/login.gts +++ b/packages/host/app/components/matrix/login.gts @@ -16,7 +16,6 @@ import moment from 'moment'; import { Button, - FieldContainer, BoxelInput, LoadingIndicator, } from '@cardstack/boxel-ui/components'; @@ -29,6 +28,9 @@ import { import type EnvironmentService from '@cardstack/host/services/environment-service'; import type MatrixService from '@cardstack/host/services/matrix-service'; +import AuthButton from './auth-button'; +import AuthFormField from './auth-form-field'; + import type { AuthMode } from './auth'; import type { LoginResponse } from 'matrix-js-sdk'; @@ -58,26 +60,21 @@ export default class Login extends Component { {{/if}}
    {{#if this.showGoogleButton}} - + {{/if}} - + { @onInput={{this.setUsername}} @onKeyPress={{this.handleEnter}} /> - - + + { @onInput={{this.setPassword}} @onKeyPress={{this.handleEnter}} /> - + - + Sign In {{#if this.error}}
    {{this.error}}
    {{/if}} @@ -155,14 +146,6 @@ export default class Login extends Component { font: var(--boxel-font-sm); line-height: 1.4; } - .field { - margin-top: var(--boxel-sp); - } - .field :deep(input:autofill) { - transition: - background-color 0s 600000s, - color 0s 600000s; - } .forgot-password { border: none; padding: 0; @@ -175,37 +158,9 @@ export default class Login extends Component { color: var(--boxel-highlight); background-color: transparent; } - .button { - --boxel-button-padding: var(--boxel-sp-sm); - width: 100%; - } - .button :deep(.boxel-loading-indicator) { - display: flex; - justify-content: center; - align-items: center; - } - .kind-primary { - --boxel-button-color: var(--auth-primary-bg); - --boxel-button-text-color: var(--auth-primary-text); - } - .kind-primary:disabled { - --boxel-button-color: var(--auth-primary-disabled-bg); - --boxel-button-text-color: var(--auth-primary-disabled-text); - --boxel-button-border: none; - } .google-button { - --boxel-button-color: var(--auth-secondary-bg); - --boxel-button-text-color: var(--auth-secondary-text); - --boxel-button-border: 1px solid var(--auth-secondary-border); - display: inline-flex; - align-items: center; - gap: var(--boxel-sp-xs); margin-top: var(--boxel-sp-sm); } - .google-button:not(:disabled):hover, - .google-button:not(:disabled):active { - --boxel-button-color: var(--auth-secondary-hover-bg); - } .google-g { width: 1.125rem; height: 1.125rem; @@ -233,7 +188,7 @@ export default class Login extends Component { display: flex; flex-wrap: wrap; justify-content: center; - gap: var(--boxel-sp-xxxs); + gap: var(--boxel-sp-3xs); margin: var(--boxel-sp) 0 0; font: 500 var(--boxel-font-sm); } @@ -271,7 +226,7 @@ export default class Login extends Component { color: var(--boxel-error-100); padding: 0; font: 500 var(--boxel-font-xs); - margin: var(--boxel-sp-xxs) auto 0 auto; + margin: var(--boxel-sp-2xs) auto 0 auto; } @@ -314,8 +269,8 @@ export default class Login extends Component { } private get isLoginButtonDisabled() { - return ( - !this.username || !this.password || this.error || this.doLogin.isRunning + return Boolean( + !this.username || !this.password || this.error || this.doLogin.isRunning, ); } diff --git a/packages/host/app/components/matrix/register-user.gts b/packages/host/app/components/matrix/register-user.gts index 813a8dd13b0..7673e56eeb0 100644 --- a/packages/host/app/components/matrix/register-user.gts +++ b/packages/host/app/components/matrix/register-user.gts @@ -12,13 +12,10 @@ import { restartableTask, timeout } from 'ember-concurrency'; import { v4 as uuidv4 } from 'uuid'; -import { LoadingIndicator } from '@cardstack/boxel-ui/components'; - import { BoxelInput, BoxelInputGroup, - Button, - FieldContainer, + LoadingIndicator, } from '@cardstack/boxel-ui/components'; import { eq } from '@cardstack/boxel-ui/helpers'; import { CheckMark } from '@cardstack/boxel-ui/icons'; @@ -33,6 +30,9 @@ import { } from '@cardstack/host/lib/matrix-utils'; import type MatrixService from '@cardstack/host/services/matrix-service'; +import AuthButton from './auth-button'; +import AuthFormField from './auth-form-field'; + import type { AuthMode } from './auth'; import type { RegisterResponse, @@ -65,14 +65,14 @@ export default class RegisterUser extends Component {
  • Leave this window open while we verify your email
  • This screen will update once your email is verified
  • - + >Resend Email {{else if (eq this.currentPage 'account-creation')}}
    Email @@ -83,12 +83,7 @@ export default class RegisterUser extends Component { {{else if (eq this.currentPage 'token-form')}} Boxel is currently invite-only.
    Enter your invite code here.
    - + { @errorMessage={{this.tokenError}} @onInput={{this.setToken}} /> - +
    - + >Next
    {{else if (eq this.currentPage 'registration-form')}} Create a Boxel Account - + { @onInput={{this.setName}} @onBlur={{this.checkName}} /> - - + + { @onInput={{this.setEmail}} @onBlur={{this.checkEmail}} /> - - + + { class='validation-hint-icon' />name available

    {{/if}} -
    - + + { class='validation-hint-icon' />password is valid

    {{/if}} -
    - + + { class='validation-hint-icon' />passwords match

    {{/if}} -
    + {{#if this.formError}}
    { >{{this.formError}}
    {{/if}}
    - - + >Login with an existing account
    {{/if}} @@ -285,8 +252,8 @@ export default class RegisterUser extends Component { .validation-hint { display: flex; align-items: center; - gap: var(--boxel-sp-xxxs); - margin: var(--boxel-sp-xxs) 0 0; + gap: var(--boxel-sp-3xs); + margin: var(--boxel-sp-2xs) 0 0; color: var(--muted-foreground); font: 500 var(--boxel-font-xs); } @@ -305,64 +272,6 @@ export default class RegisterUser extends Component { align-items: center; gap: var(--boxel-sp-sm); } - .button { - --boxel-button-padding: var(--boxel-sp-xs) var(--boxel-sp-lg); - --boxel-button-font: 600 var(--boxel-font-sm); - letter-spacing: var(--boxel-lsp); - width: 100%; - } - .kind-primary { - --boxel-button-color: var(--auth-primary-bg); - --boxel-button-text-color: var(--auth-primary-text); - } - .kind-primary:disabled { - --boxel-button-color: var(--auth-primary-disabled-bg); - --boxel-button-text-color: var(--auth-primary-disabled-text); - --boxel-button-border: none; - } - .secondary-cta { - --boxel-button-color: var(--auth-secondary-bg); - --boxel-button-text-color: var(--auth-secondary-text); - --boxel-button-border: 1px solid var(--auth-secondary-border); - } - .secondary-cta:not(:disabled):hover, - .secondary-cta:not(:disabled):active { - --boxel-button-color: var(--auth-secondary-hover-bg); - } - .registration-field { - margin-top: var(--boxel-sp); - } - .registration-field :deep(.text-accessory) { - color: var(--muted-foreground); - } - .registration-field :deep(.validation-icon-container.invalid) { - display: none; - } - .registration-field :deep(.validation-icon-container.valid svg) { - height: var(--boxel-sp-xs); - } - .registration-field - :deep(.boxel-input-group--invalid > :nth-last-child(2)) { - border-top-right-radius: var(--boxel-input-group-border-radius); - border-bottom-right-radius: var(--boxel-input-group-border-radius); - border-right-width: var(--boxel-input-group-interior-border-width); - } - .registration-field - :deep( - .boxel-input-group:not(.boxel-input-group--invalid) - > :nth-last-child(2) - ) { - padding-right: 0; - } - .registration-field :deep(input:autofill) { - transition: - background-color 0s 600000s, - color 0s 600000s; - } - .registration-field :deep(.error-message) { - margin-left: 0; - font: 500 var(--boxel-font-xs); - } .username-prefix { padding-right: 0; } @@ -377,10 +286,6 @@ export default class RegisterUser extends Component { margin-bottom: var(--boxel-sp-sm); } .resend-email { - --boxel-button-padding: var(--boxel-sp-sm) var(--boxel-sp-lg); - --boxel-button-font: 600 var(--boxel-font-sm); - letter-spacing: var(--boxel-lsp); - width: 100%; margin-top: var(--boxel-sp); } .error-message { @@ -485,10 +390,10 @@ export default class RegisterUser extends Component { } private get isRegisterButtonDisabled() { - return ( + return Boolean( this.hasRegistrationMissingField || this.hasRegistrationError || - this.doRegistrationFlow.isRunning + this.doRegistrationFlow.isRunning, ); }