Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 36 additions & 34 deletions packages/experience/src/Providers/AppBoundary/AppMeta.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { Theme } from '@logto/schemas';
import { conditionalString } from '@silverhand/essentials';
import classNames from 'classnames';
import i18next from 'i18next';
import { useContext } from 'react';
import { Helmet } from 'react-helmet';
import { Theme } from '@logto/schemas'
import { conditionalString } from '@silverhand/essentials'
import classNames from 'classnames'
import i18next from 'i18next'
import { useContext } from 'react'
import { Helmet } from 'react-helmet'

import PageContext from '@/Providers/PageContextProvider/PageContext';
import defaultAppleTouchLogo from '@/assets/apple-touch-icon.png';
import defaultFavicon from '@/assets/favicon.png';
import { type SignInExperienceResponse } from '@/types';
import PageContext from '@/Providers/PageContextProvider/PageContext'
import defaultAppleTouchLogo from '@/assets/apple-touch-icon.png'
import defaultFavicon from '@/assets/favicon.png'
import { type SignInExperienceResponse } from '@/types'

import styles from './index.module.scss';
import styles from './index.module.scss'
import bitfocusCustomStyles from './bitfocus-custom-styles.css?raw'

const themeToFavicon = Object.freeze({
[Theme.Light]: 'favicon',
[Theme.Dark]: 'darkFavicon',
} as const satisfies Record<Theme, keyof SignInExperienceResponse['branding']>);
[Theme.Light]: 'favicon',
[Theme.Dark]: 'darkFavicon',
} as const satisfies Record<Theme, keyof SignInExperienceResponse['branding']>)

/**
* User React Helmet to manage html and body attributes
Expand All @@ -32,25 +33,26 @@ const themeToFavicon = Object.freeze({
*/

const AppMeta = () => {
const { experienceSettings, theme, platform, isPreview } = useContext(PageContext);
const favicon =
experienceSettings?.branding[themeToFavicon[theme]] ?? experienceSettings?.branding.favicon;
const { experienceSettings, theme, platform, isPreview } = useContext(PageContext)
const favicon = experienceSettings?.branding[themeToFavicon[theme]] ?? experienceSettings?.branding.favicon

return (
<Helmet>
<html lang={i18next.language} dir={i18next.dir()} data-theme={theme} />
<link rel="shortcut icon" href={favicon ?? defaultFavicon} />
<link rel="apple-touch-icon" href={favicon ?? defaultAppleTouchLogo} sizes="180x180" />
{experienceSettings?.customCss && <style>{experienceSettings.customCss}</style>}
<body
className={classNames(
conditionalString(isPreview && styles.preview),
platform === 'mobile' ? 'mobile' : 'desktop',
conditionalString(styles[theme])
)}
/>
</Helmet>
);
};
return (
<Helmet>
<html lang={i18next.language} dir={i18next.dir()} data-theme={theme} />
<link rel="shortcut icon" href={favicon ?? defaultFavicon} />
<link rel="apple-touch-icon" href={favicon ?? defaultAppleTouchLogo} sizes="180x180" />
{/* make sure the experienceSettings.customCss overrides the customStyles */}
{bitfocusCustomStyles && <style>{bitfocusCustomStyles}</style>}
{experienceSettings?.customCss && <style>{experienceSettings.customCss}</style>}
<body
className={classNames(
conditionalString(isPreview && styles.preview),
platform === 'mobile' ? 'mobile' : 'desktop',
conditionalString(styles[theme]),
)}
/>
</Helmet>
)
}

export default AppMeta;
export default AppMeta
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100 900; /* supports variable weights */
font-display: swap;
src: url('/fonts/inter/Inter-VariableFont_slnt,wght.woff2') format('woff2');
}

#app * {
font-family: 'Inter', sans-serif;
}

/* Main background - matches bg-neutral-950 */
#app > div[class$='viewBox'] {
background: #0a0a0a;
min-height: 100vh;
}
Comment on lines +14 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The custom styles appear to be designed only for a dark theme, with hardcoded dark background and light text colors. However, the application supports both light and dark themes via a data-theme attribute on the <html> tag. These styles do not respect the current theme, which will result in poor readability and an incorrect appearance in light mode.

You should adapt these styles to work with both themes. You can use the [data-theme='light'] and [data-theme='dark'] selectors to provide theme-specific styles.

For example:

/* Default (dark theme) styles */
#app > div[class$='viewBox'] {
  background: #0a0a0a;
}

/* Light theme overrides */
[data-theme='light'] #app > div[class$='viewBox'] {
  background: #ffffff; /* Example light color */
}

Alternatively, using CSS variables that change based on the theme would be an even better approach for maintainability.


#app main[class*='main'] {
background: transparent;
max-width: 460px;
margin: 0 auto;
padding: 64px 16px 40px;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
}

#app main[class*='main'] div[class*='logoWrapper'] {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}

#app main[class*='main'] div[class*='headline'] {
color: #d4d4d4;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line has trailing whitespace. While this doesn't affect functionality, it's good practice to remove it to keep the code clean and consistent. Many code editors can be configured to do this automatically on save.

  color: #d4d4d4;


#app main[class*='main'] img[class*='logo'] {
content: url('src/assets/buttons.png');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The path url('src/assets/buttons.png') is unlikely to work correctly in a production build. This CSS is injected as a raw string into a <style> tag in the document's <head>. The URL will be resolved relative to the page's URL, not the CSS file's source location. This means the browser will try to fetch https://<your-domain>/src/assets/buttons.png, which will likely result in a 404 error.

To fix this, you should use a path that is correct relative to the final deployment structure (e.g., an absolute path like /assets/buttons.png if your assets are served from an assets directory at the root).

A more robust solution would be to import the image in your TypeScript/JavaScript code to let the bundler (Vite) handle it, and then pass the resulting URL to your CSS, for example via a CSS variable. Another option is to inline the image as a data URI if it's small.

width: 64px;
height: 64px;
margin: 0;
}

/* Title styling - matches text-3xl font-semibold tracking-tight */
#app main[class*='main'] h1,
#app main[class*='main'] h2 {
font-size: 1.875rem;
font-weight: 600;
letter-spacing: -0.025em;
color: #d4d4d8; /* text-neutral-300 */
text-align: center;
margin: 0;
}

/* Subtitle styling */
#app main[class*='main'] h2 {
font-size: 1.5rem;
margin-top: 8px;
}

/* Description text - matches text-sm text-neutral-300 text-center */
#app main[class*='main'] p {
font-size: 0.875rem;
color: #d4d4d8; /* text-neutral-300 */
text-align: center;
margin: 16px 0;
line-height: 1.5;
}

#app main[class*='main'] > div[class*='wrapper'] > div[class*='container'] {
margin: 16px auto;
}

/* Form container - matches rounded-md border border-neutral-800 bg-neutral-900 p-5 shadow-xl */
#app main[class*='main'] > div[class*='wrapper'] {
background: #171717; /* bg-neutral-900 */
border: 1px solid #262626; /* border-neutral-800 */
border-radius: 6px; /* rounded-md */
padding: 32px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
margin-top: 16px;
flex: 0;
}

/* Form styling */
#app form {
display: flex;
flex-direction: column;
gap: 4px;
}

/* Label styling - matches text-sm font-medium text-neutral-200 */
#app form label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #e5e5e5; /* text-neutral-200 */
margin-bottom: 4px;
}

/* Input field container */
#app form div[class*='inputField'] > div {
outline: none;
border: none;
border-radius: 4px;
background: rgba(38, 38, 38, 0.2); /* bg-neutral-800/20 */
}

/* Input field action button */
#app form div[class*='inputField'] > button {
align-items: end;
}

/* Input styling - matches mt-1 block w-full border-gray-300 rounded-sm shadow-sm bg-neutral-800/20 px-2 py-1 text-lg */
#app form div[class*='inputField'] input,
#app form div[class*='inputField'] div[class$='countryCodeSelector'] {
background: rgba(38, 38, 38, 0.2); /* bg-neutral-800/20 */
font-family: 'Inter', sans-serif;
font-size: 1.125rem; /* text-lg */
font-weight: 400;
color: #d4d4d8; /* text-neutral-300 */
border: 1px solid #404040; /* subtle border */
border-radius: 4px; /* rounded-sm */
padding: 4px 8px; /* px-2 py-1 */
width: 100%;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}

/* Input focus state */
#app form div[class*='inputField'] input:focus,
#app form div[class*='inputField'] div[class$='countryCodeSelector']:focus {
outline: none;
border-color: #3b82f6; /* focus ring color */
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

/* Placeholder styling */
#app form div[class*='inputField'] > div > input::placeholder,
#app main[class*='main'] > div[class*='wrapper'] > div[class*='divider'],
#app main[class*='main'] > div[class*='wrapper'] > form div[class*='content'],
#app main[class*='main'] > div[class*='wrapper'] > form div[class*='content'] > span {
color: #a3a3a3; /* neutral-400 for placeholders */
}

/* Error message styling - matches text-red-400 text-xs mt-1 */
#app form div[class*='error'] {
color: #f87171; /* text-red-400 */
font-size: 0.75rem; /* text-xs */
margin-top: 4px;
}

/* Button styling - matches the submit button from FirstUserPage */
#app button {
font-weight: 600;
font-size: 1.125rem; /* text-lg */
border-radius: 6px; /* rounded-md */
padding: 6px 0; /* py-1.5 */
width: 100%;
transition: all 0.2s;
border: none;
cursor: pointer;
}

/* Submit button - matches bg-bitfocus-500 styling */
#app button[type='submit'] {
background: #F0BE35;
color: #0f172a; /* text-neutral-900 */
opacity: 0.9;
}

#app button[type='submit']:hover:not(:disabled) {
opacity: 1;
}

#app button[type='submit']:disabled {
background: rgba(82, 82, 82, 0.1); /* bg-neutral-500/10 */
color: #404040; /* text-neutral-700 */
cursor: not-allowed;
}

/* Social login buttons */
#app div[class*='socialLinkList'] > button {
border: none;
background-color: #3b82f6; /* bitfocus-500 */
color: #0f172a;
Comment on lines +189 to +190
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There seems to be an inconsistency with the color value for bitfocus-500. Here, background-color is set to #3b82f6 (blue) with a comment /* bitfocus-500 */. However, on line 171, the submit button's background is set to #F0BE35 (yellow) with a comment matches bg-bitfocus-500 styling.

Please verify which color is the correct one for bitfocus-500 and update the CSS and/or comments accordingly to avoid confusion.

margin-bottom: 8px;
}

/* SSO label */
#app [class*='singleSignOn'] {
color: #d4d4d8; /* text-neutral-300 */
font-weight: 400;
}

#app [class*='navBar'] {
color: #d4d4d8; /* text-neutral-300 */
font-weight: 800;
}
#app [class*='title'] {
color: #d4d4d8; /* text-neutral-300 */
font-weight: 800;
}

/* Links */
#app a[class*='primary'] {
color: #d4d4d8; /* text-neutral-300 */
font-weight: 800;
}

/* Status/error messages - matches text-yellow-500 text-center mt-4 */
#app div[class*='status'] {
color: #eab308; /* text-yellow-500 */
text-align: center;
margin-top: 16px;
font-size: 0.875rem;
}

/* Hide the clear (suffix) button on identifier input */
#app form div[class*='inputField'] [class$='suffix'] {
display: none !important;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of !important should generally be avoided as it can make stylesheets difficult to debug and override later. It breaks the natural cascade of CSS.

Please check if you can achieve the same result by increasing the selector's specificity. For example, a more specific selector might be able to override the display property without needing !important.

If !important is truly necessary here to override an inline style or another !important rule, it would be good to add a comment explaining why it's needed.

}

/* Hide native clear buttons in various browsers */
#app form div[class*='inputField'] input::-ms-clear,
#app form div[class*='inputField'] input::-ms-reveal {
display: none;
width: 0;
height: 0;
}

#app form div[class*='inputField'] input[type='search']::-webkit-search-cancel-button,
#app form div[class*='inputField'] input::-webkit-clear-button {
-webkit-appearance: none;
appearance: none;
display: none;
}
Loading