diff --git a/.changeset-staged/quick-schools-complain.md b/.changeset-staged/quick-schools-complain.md new file mode 100644 index 000000000..ebbcce19b --- /dev/null +++ b/.changeset-staged/quick-schools-complain.md @@ -0,0 +1,8 @@ +--- +"@logto/console": minor +"@logto/phrases": minor +"@logto/ui": minor +--- + +Add custom CSS code editor so that users can apply advanced UI customization. + - Users can check the real time preview of the CSS via SIE preview on the right side. diff --git a/packages/console/src/components/SignInExperiencePreview/index.tsx b/packages/console/src/components/SignInExperiencePreview/index.tsx index dce70c27f..3aeb62c32 100644 --- a/packages/console/src/components/SignInExperiencePreview/index.tsx +++ b/packages/console/src/components/SignInExperiencePreview/index.tsx @@ -29,6 +29,7 @@ const SignInExperiencePreview = ({ platform, mode, language = 'en', signInExperi const { customPhrases } = useUiLanguages(); const { userEndpoint } = useContext(AppEndpointsContext); const previewRef = useRef(null); + const customCssRef = useRef(document.createElement('style')); const { data: allConnectors } = useSWR('api/connectors'); const configForUiPage = useMemo(() => { @@ -80,6 +81,8 @@ const SignInExperiencePreview = ({ platform, mode, language = 'en', signInExperi useEffect(() => { postPreviewMessage(); + document.head.append(customCssRef.current); + const iframe = previewRef.current; iframe?.addEventListener('load', postPreviewMessage); diff --git a/packages/console/src/pages/SignInExperience/tabs/Branding/CustomCssForm.tsx b/packages/console/src/pages/SignInExperience/tabs/Branding/CustomCssForm.tsx new file mode 100644 index 000000000..875f79f41 --- /dev/null +++ b/packages/console/src/pages/SignInExperience/tabs/Branding/CustomCssForm.tsx @@ -0,0 +1,60 @@ +import { useFormContext, Controller } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; + +import Card from '@/components/Card'; +import CodeEditor from '@/components/CodeEditor'; +import FormField from '@/components/FormField'; +import TextLink from '@/components/TextLink'; +import useDocumentationUrl from '@/hooks/use-documentation-url'; + +import type { SignInExperienceForm } from '../../types'; +import * as tabsStyles from '../index.module.scss'; +import * as brandingStyles from './index.module.scss'; + +const CustomCssForm = () => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { getDocumentationUrl } = useDocumentationUrl(); + const { control } = useFormContext(); + + return ( + +
{t('sign_in_exp.custom_css.title')}
+ ( + + ), + }} + > + {t('sign_in_exp.custom_css.css_code_editor_description', { + link: t('sign_in_exp.custom_css.css_code_editor_description_link_content'), + })} + + )} + > + ( + + )} + /> + +
+ ); +}; + +export default CustomCssForm; diff --git a/packages/console/src/pages/SignInExperience/tabs/Branding/index.module.scss b/packages/console/src/pages/SignInExperience/tabs/Branding/index.module.scss new file mode 100644 index 000000000..c9966d410 --- /dev/null +++ b/packages/console/src/pages/SignInExperience/tabs/Branding/index.module.scss @@ -0,0 +1,5 @@ +.customCssCodeEditor { + max-height: calc(100vh - 260px); + min-height: 111px; // min-height to show three lines of code + overflow-y: auto; +} diff --git a/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx b/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx index 550f89db2..73b639fb8 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx @@ -2,6 +2,7 @@ import TabWrapper from '../../components/TabWrapper'; import * as styles from '../index.module.scss'; import BrandingForm from './BrandingForm'; import ColorForm from './ColorForm'; +import CustomCssForm from './CustomCssForm'; type Props = { isActive: boolean; @@ -11,6 +12,7 @@ const Branding = ({ isActive }: Props) => ( + ); diff --git a/packages/console/src/pages/SignInExperience/utils/form.ts b/packages/console/src/pages/SignInExperience/utils/form.ts index fea388a09..7c8953d88 100644 --- a/packages/console/src/pages/SignInExperience/utils/form.ts +++ b/packages/console/src/pages/SignInExperience/utils/form.ts @@ -40,7 +40,7 @@ export const signInExperienceParser = { }; }, toRemoteModel: (setup: SignInExperienceForm): SignInExperience => { - const { branding, createAccountEnabled, signUp } = setup; + const { branding, createAccountEnabled, signUp, customCss } = setup; return { ...setup, @@ -59,6 +59,7 @@ export const signInExperienceParser = { verify: false, }, signInMode: createAccountEnabled ? SignInMode.SignInAndRegister : SignInMode.SignIn, + customCss: customCss?.length ? customCss : null, }; }, }; @@ -80,11 +81,12 @@ export const hasSignUpAndSignInConfigChanged = ( export const getBrandingErrorCount = ( errors: FieldErrorsImpl> ) => { - const { color, branding } = errors; + const { color, branding, customCss } = errors; const colorFormErrorCount = color ? Object.keys(color).length : 0; const brandingFormErrorCount = branding ? Object.keys(branding).length : 0; + const customCssFormErrorCount = customCss ? 1 : 0; - return colorFormErrorCount + brandingFormErrorCount; + return colorFormErrorCount + brandingFormErrorCount + customCssFormErrorCount; }; export const getSignUpAndSignInErrorCount = ( diff --git a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts index 3ab21fa52..d018c20f3 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts @@ -90,6 +90,12 @@ const sign_in_exp = { dark_logo_image_url: 'App logo URL (Dunkler Modus)', dark_logo_image_url_placeholder: 'https://dein.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, others: { terms_of_use: { title: 'NUTZUNGSBEDINGUNGEN', diff --git a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts index 7eb8570e9..a3d499895 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts @@ -33,6 +33,12 @@ const sign_in_exp = { dark_logo_image_url: 'App logo image URL (Dark)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: 'Email address', identifiers_phone: 'Phone number', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts index f2c6c7b15..92b6eda4d 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts @@ -35,6 +35,12 @@ const sign_in_exp = { dark_logo_image_url: "URL de l'image du logo de l'application (Sombre)", dark_logo_image_url_placeholder: 'https://votre.domaine.cdn/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: 'Email address', // UNTRANSLATED identifiers_phone: 'Phone number', // UNTRANSLATED diff --git a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts index 5f22760ad..249545dbc 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts @@ -31,6 +31,12 @@ const sign_in_exp = { dark_logo_image_url: '앱 로고 이미지 URL (다크 모드)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: '이메일 주소', identifiers_phone: '휴대전화번호', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts index 9b7344033..0d9d426a7 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts @@ -34,6 +34,12 @@ const sign_in_exp = { dark_logo_image_url: 'URL da imagem do logotipo do aplicativo (Escuro)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: 'Endereço de e-mail', identifiers_phone: 'Número de telefone', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts index 176045ef7..94ddfa3ea 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts @@ -33,6 +33,12 @@ const sign_in_exp = { dark_logo_image_url: 'URL do logotipo da app (tema escuro)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: 'Email address', // UNTRANSLATED identifiers_phone: 'Phone number', // UNTRANSLATED diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts index 49df61aab..be76ef9b8 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts @@ -34,6 +34,12 @@ const sign_in_exp = { dark_logo_image_url: 'Uygulama logosu resim URLi (Koyu)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: 'Email address', // UNTRANSLATED identifiers_phone: 'Phone number', // UNTRANSLATED diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts index e05399487..6ca8a5c6c 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts @@ -32,6 +32,12 @@ const sign_in_exp = { dark_logo_image_url: 'Logo 图片 URL (深色)', dark_logo_image_url_placeholder: 'https://your.cdn.domain/logo-dark.png', }, + custom_css: { + title: 'CUSTOM CSS', // UNTRANSLATED + css_code_editor_title: 'Custom CSS to change UI', // UNTRANSLATED + css_code_editor_description: 'Description - Doc. {{link}}', // UNTRANSLATED + css_code_editor_description_link_content: 'Readme', // UNTRANSLATED + }, sign_up_and_sign_in: { identifiers_email: '邮件地址', identifiers_phone: '手机号码', diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index f56f8b03c..65d8b3e7d 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -29,10 +29,9 @@ import './scss/normalized.scss'; const App = () => { const { context, Provider } = usePageContext(); - const { isPreview, experienceSettings, setLoading, setExperienceSettings } = context; + const { experienceSettings, setLoading, setExperienceSettings } = context; const customCssRef = useRef(document.createElement('style')); - - usePreview(context); + const [isPreview, previewConfig] = usePreview(context); useEffect(() => { document.head.append(customCssRef.current); @@ -40,6 +39,9 @@ const App = () => { useEffect(() => { if (isPreview) { + // eslint-disable-next-line @silverhand/fp/no-mutation + customCssRef.current.textContent = previewConfig?.signInExperience.customCss ?? null; + return; } @@ -61,7 +63,7 @@ const App = () => { // Init the page settings and render setExperienceSettings(settings); })(); - }, [isPreview, setExperienceSettings, setLoading]); + }, [isPreview, previewConfig, setExperienceSettings, setLoading]); if (!experienceSettings) { return null;