diff --git a/.changeset/five-geese-retire.md b/.changeset/five-geese-retire.md new file mode 100644 index 00000000..1c9e6fe9 --- /dev/null +++ b/.changeset/five-geese-retire.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/react': patch +--- + +- Fix import path for useAsgardeo in SignedOut component +- Rename `AsgardeoLoading` -> `Loading` diff --git a/.changeset/public-donkeys-tickle.md b/.changeset/public-donkeys-tickle.md new file mode 100644 index 00000000..35000081 --- /dev/null +++ b/.changeset/public-donkeys-tickle.md @@ -0,0 +1,8 @@ +--- +'@asgardeo/javascript': patch +'@asgardeo/nextjs': patch +'@asgardeo/node': patch +'@asgardeo/react': patch +--- + +Fix `getAccessToken` imperative usage diff --git a/packages/javascript/src/api/updateMeProfile.ts b/packages/javascript/src/api/updateMeProfile.ts index 90c376f6..ff99cd84 100644 --- a/packages/javascript/src/api/updateMeProfile.ts +++ b/packages/javascript/src/api/updateMeProfile.ts @@ -147,10 +147,11 @@ const updateMeProfile = async ({ } throw new AsgardeoAPIError( - `Network or parsing error: ${error instanceof Error ? error.message : 'Unknown error'}`, + error?.response?.data?.detail || + 'An error occurred while updating the user profile. Please try again.', 'updateMeProfile-NetworkError-001', 'javascript', - 0, + error?.data?.status, 'Network Error', ); } diff --git a/packages/javascript/src/errors/AsgardeoError.ts b/packages/javascript/src/errors/AsgardeoError.ts index 5d34f6cb..2ae81905 100644 --- a/packages/javascript/src/errors/AsgardeoError.ts +++ b/packages/javascript/src/errors/AsgardeoError.ts @@ -55,12 +55,7 @@ export default class AsgardeoError extends Error { constructor(message: string, code: string, origin: string) { const _origin: string = AsgardeoError.resolveOrigin(origin); - const prefix: string = `🛡️ Asgardeo - ${_origin}:`; - const regex: RegExp = new RegExp(`🛡️\\s*Asgardeo\\s*-\\s*${_origin}:`, 'i'); - const sanitized: string = message.replace(regex, ''); - const _message: string = `${prefix} ${sanitized.trim()}\n\n(code="${code}")\n`; - - super(_message); + super(message); this.name = new.target.name; this.code = code; @@ -72,6 +67,7 @@ export default class AsgardeoError extends Error { } public override toString(): string { - return `[${this.name}]\nMessage: ${this.message}`; + const prefix: string = `🛡️ Asgardeo - ${this.origin}:`; + return `[${this.name}]\n${prefix} ${this.message}\n(code=\"${this.code}\")`; } } diff --git a/packages/javascript/src/i18n/en-US.ts b/packages/javascript/src/i18n/en-US.ts index 31242dda..789135a9 100644 --- a/packages/javascript/src/i18n/en-US.ts +++ b/packages/javascript/src/i18n/en-US.ts @@ -78,6 +78,13 @@ const translations: I18nTranslations = { 'username.password.title': 'Sign In', 'username.password.subtitle': 'Enter your username and password to continue.', + /* |---------------------------------------------------------------| */ + /* | User Profile | */ + /* |---------------------------------------------------------------| */ + + 'user.profile.title': 'Profile', + 'user.profile.update.generic.error': 'An error occurred while updating your profile. Please try again.', + /* |---------------------------------------------------------------| */ /* | Organization Switcher | */ /* |---------------------------------------------------------------| */ diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index c01d490f..813f10e2 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -112,6 +112,8 @@ export {default as AsgardeoJavaScriptClient} from './AsgardeoJavaScriptClient'; export {default as createTheme} from './theme/createTheme'; export {ThemeColors, ThemeConfig, Theme, ThemeMode, ThemeDetection} from './theme/types'; +export {default as bem} from './utils/bem'; +export {default as formatDate} from './utils/formatDate'; export {default as processUsername} from './utils/processUsername'; export {default as deepMerge} from './utils/deepMerge'; export {default as deriveOrganizationHandleFromBaseUrl} from './utils/deriveOrganizationHandleFromBaseUrl'; diff --git a/packages/javascript/src/models/i18n.ts b/packages/javascript/src/models/i18n.ts index dbdf9d27..39afb9c9 100644 --- a/packages/javascript/src/models/i18n.ts +++ b/packages/javascript/src/models/i18n.ts @@ -76,6 +76,13 @@ export interface I18nTranslations { 'username.password.title': string; 'username.password.subtitle': string; + /* |---------------------------------------------------------------| */ + /* | User Profile | */ + /* |---------------------------------------------------------------| */ + + 'user.profile.title': string; + 'user.profile.update.generic.error': string; + /* |---------------------------------------------------------------| */ /* | Organization Switcher | */ /* |---------------------------------------------------------------| */ diff --git a/packages/javascript/src/theme/createTheme.ts b/packages/javascript/src/theme/createTheme.ts index 59bc9393..323a3554 100644 --- a/packages/javascript/src/theme/createTheme.ts +++ b/packages/javascript/src/theme/createTheme.ts @@ -54,33 +54,46 @@ const lightTheme: ThemeConfig = { primary: { main: '#1a73e8', contrastText: '#ffffff', + dark: '#174ea6', }, secondary: { main: '#424242', contrastText: '#ffffff', + dark: '#212121', }, background: { surface: '#ffffff', disabled: '#f0f0f0', + dark: '#212121', body: { main: '#1a1a1a', + dark: '#212121', }, }, error: { main: '#d32f2f', - contrastText: '#ffffff', + contrastText: '#d52828', + dark: '#b71c1c', + }, + info: { + main: '#bbebff', + contrastText: '#43aeda', + dark: '#01579b', }, success: { main: '#4caf50', - contrastText: '#ffffff', + contrastText: '#00a807', + dark: '#388e3c', }, warning: { main: '#ff9800', - contrastText: '#ffffff', + contrastText: '#be7100', + dark: '#f57c00', }, text: { primary: '#1a1a1a', secondary: '#666666', + dark: '#212121', }, border: '#e0e0e0', }, @@ -144,33 +157,46 @@ const darkTheme: ThemeConfig = { primary: { main: '#1a73e8', contrastText: '#ffffff', + dark: '#174ea6', }, secondary: { main: '#424242', contrastText: '#ffffff', + dark: '#212121', }, background: { surface: '#121212', disabled: '#1f1f1f', + dark: '#212121', body: { main: '#ffffff', + dark: '#212121', }, }, error: { main: '#d32f2f', - contrastText: '#ffffff', + contrastText: '#d52828', + dark: '#b71c1c', + }, + info: { + main: '#bbebff', + contrastText: '#43aeda', + dark: '#01579b', }, success: { main: '#4caf50', - contrastText: '#ffffff', + contrastText: '#00a807', + dark: '#388e3c', }, warning: { main: '#ff9800', - contrastText: '#ffffff', + contrastText: '#be7100', + dark: '#f57c00', }, text: { primary: '#ffffff', secondary: '#b3b3b3', + dark: '#212121', }, border: '#404040', }, @@ -306,6 +332,14 @@ const toCssVariables = (theme: ThemeConfig): Record => { cssVars[`--${prefix}-color-warning-contrastText`] = theme.colors.warning.contrastText; } + // Colors - Info + if (theme.colors?.info?.main) { + cssVars[`--${prefix}-color-info-main`] = theme.colors.info.main; + } + if (theme.colors?.info?.contrastText) { + cssVars[`--${prefix}-color-info-contrastText`] = theme.colors.info.contrastText; + } + // Colors - Text if (theme.colors?.text?.primary) { cssVars[`--${prefix}-color-text-primary`] = theme.colors.text.primary; @@ -455,6 +489,10 @@ const toThemeVars = (theme: ThemeConfig): ThemeVars => { main: `var(--${prefix}-color-error-main)`, contrastText: `var(--${prefix}-color-error-contrastText)`, }, + info: { + contrastText: `var(--${prefix}-color-info-contrastText)`, + main: `var(--${prefix}-color-info-main)`, + }, success: { main: `var(--${prefix}-color-success-main)`, contrastText: `var(--${prefix}-color-success-contrastText)`, diff --git a/packages/javascript/src/theme/types.ts b/packages/javascript/src/theme/types.ts index 9fee3ad6..19aea3a2 100644 --- a/packages/javascript/src/theme/types.ts +++ b/packages/javascript/src/theme/types.ts @@ -57,34 +57,47 @@ export interface ThemeColors { background: { body: { main: string; + dark?: string; }; disabled: string; surface: string; + dark?: string; }; border: string; error: { contrastText: string; main: string; + dark?: string; + }; + info: { + contrastText: string; + main: string; + dark?: string; }; primary: { contrastText: string; main: string; + dark?: string; }; secondary: { contrastText: string; main: string; + dark?: string; }; success: { contrastText: string; main: string; + dark?: string; }; text: { primary: string; secondary: string; + dark?: string; }; warning: { contrastText: string; main: string; + dark?: string; }; } @@ -155,33 +168,46 @@ export interface ThemeVars { primary: { main: string; contrastText: string; + dark?: string; }; secondary: { main: string; contrastText: string; + dark?: string; }; background: { surface: string; disabled: string; + dark?: string; body: { main: string; + dark?: string; }; }; error: { main: string; contrastText: string; + dark?: string; + }; + info: { + contrastText: string; + main: string; + dark?: string; }; success: { main: string; contrastText: string; + dark?: string; }; warning: { main: string; contrastText: string; + dark?: string; }; text: { primary: string; secondary: string; + dark?: string; }; border: string; }; diff --git a/packages/javascript/src/utils/bem.ts b/packages/javascript/src/utils/bem.ts new file mode 100644 index 00000000..71f90128 --- /dev/null +++ b/packages/javascript/src/utils/bem.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Creates a BEM-style class name by combining a base class with element and/or modifier + * + * @param baseClass - The base CSS class string (usually from emotion's css function) + * @param element - The BEM element name (optional) + * @param modifier - The BEM modifier name (optional) + * @returns The combined class name string + * + * @example + * ```tsx + * const baseClass = css` + * display: flex; + * &__element { + * color: red; + * } + * &--modifier { + * background: blue; + * } + * `; + * + * import bem from './utils/bem'; + * + * const elementClass = bem(baseClass, 'element'); + * const modifierClass = bem(baseClass, null, 'modifier'); + * const elementWithModifierClass = bem(baseClass, 'element', 'modifier'); + * ``` + */ +const bem = (baseClass: string, element?: string | null, modifier?: string | null): string => { + let className = baseClass; + + if (element) { + className += `__${element}`; + } + + if (modifier) { + className += `--${modifier}`; + } + + return className; +}; + +export default bem; diff --git a/packages/javascript/src/utils/formatDate.ts b/packages/javascript/src/utils/formatDate.ts new file mode 100644 index 00000000..0c34fa35 --- /dev/null +++ b/packages/javascript/src/utils/formatDate.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Formats a date string to a human-readable format. + * + * @param dateString - The date string to format (optional) + * @returns A formatted date string in 'Month Day, Year' format, or '-' if no date is provided, or the original string if parsing fails + * + * @example + * ```typescript + * formatDate('2025-07-09T10:30:00Z'); // Returns "July 9, 2025" + * formatDate(''); // Returns "-" + * formatDate(undefined); // Returns "-" + * formatDate('invalid-date'); // Returns "invalid-date" + * ``` + */ +const formatDate = (dateString?: string): string => { + if (!dateString) return '-'; + + try { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + } catch { + return dateString; + } +}; + +export default formatDate; diff --git a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts index 4f04984f..494c1a23 100644 --- a/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts +++ b/packages/javascript/src/utils/transformBrandingPreferenceToTheme.ts @@ -23,7 +23,13 @@ import createTheme from '../theme/createTheme'; /** * Safely extracts a color value from the branding preference structure */ -const extractColorValue = (colorVariant?: {main?: string; contrastText?: string}) => { +type ColorVariant = {main?: string; dark?: string; contrastText?: string}; +type TextColors = {primary?: string; secondary?: string; dark?: string}; + +const extractColorValue = (colorVariant?: ColorVariant, preferDark = false): string | undefined => { + if (preferDark && colorVariant?.dark && colorVariant.dark.trim()) { + return colorVariant.dark; + } return colorVariant?.main; }; @@ -59,36 +65,50 @@ const transformThemeVariant = (themeVariant: ThemeVariant, isDark = false): Part activatedOpacity: 0.12, }, primary: { - main: extractColorValue(colors?.primary), + main: extractColorValue(colors?.primary as ColorVariant, isDark), contrastText: extractContrastText(colors?.primary), + dark: colors?.primary?.dark || (colors?.primary as ColorVariant)?.main, }, secondary: { - main: extractColorValue(colors?.secondary), + main: extractColorValue(colors?.secondary as ColorVariant, isDark), contrastText: extractContrastText(colors?.secondary), + dark: colors?.secondary?.dark || (colors?.secondary as ColorVariant)?.main, }, background: { - surface: extractColorValue(colors?.background?.surface), - disabled: extractColorValue(colors?.background?.surface), + surface: extractColorValue(colors?.background?.surface as ColorVariant, isDark), + disabled: extractColorValue(colors?.background?.surface as ColorVariant, isDark), + dark: + (colors?.background?.surface as ColorVariant)?.dark || (colors?.background?.surface as ColorVariant)?.main, body: { - main: extractColorValue(colors?.background?.body), + main: extractColorValue(colors?.background?.body as ColorVariant, isDark), + dark: (colors?.background?.body as ColorVariant)?.dark || (colors?.background?.body as ColorVariant)?.main, }, }, text: { - primary: colors?.text?.primary, - secondary: colors?.text?.secondary, + primary: (colors?.text as TextColors)?.primary, + secondary: (colors?.text as TextColors)?.secondary, + dark: (colors?.text as TextColors)?.dark || (colors?.text as TextColors)?.primary, }, border: colors?.outlined?.default, error: { - main: extractColorValue(colors?.alerts?.error), + main: extractColorValue(colors?.alerts?.error as ColorVariant, isDark), contrastText: extractContrastText(colors?.alerts?.error), + dark: (colors?.alerts?.error as ColorVariant)?.dark || (colors?.alerts?.error as ColorVariant)?.main, }, - success: { - main: extractColorValue(colors?.alerts?.info), + info: { + main: extractColorValue(colors?.alerts?.info as ColorVariant, isDark), contrastText: extractContrastText(colors?.alerts?.info), + dark: (colors?.alerts?.info as ColorVariant)?.dark || (colors?.alerts?.info as ColorVariant)?.main, + }, + success: { + main: extractColorValue(colors?.alerts?.neutral as ColorVariant, isDark), + contrastText: extractContrastText(colors?.alerts?.neutral), + dark: (colors?.alerts?.neutral as ColorVariant)?.dark || (colors?.alerts?.neutral as ColorVariant)?.main, }, warning: { - main: extractColorValue(colors?.alerts?.warning), + main: extractColorValue(colors?.alerts?.warning as ColorVariant, isDark), contrastText: extractContrastText(colors?.alerts?.warning), + dark: (colors?.alerts?.warning as ColorVariant)?.dark || (colors?.alerts?.warning as ColorVariant)?.main, }, }, // Extract border radius from buttons or inputs diff --git a/packages/react/src/components/control/AsgardeoLoading.tsx b/packages/nextjs/src/client/components/control/Loading/Loading.tsx similarity index 72% rename from packages/react/src/components/control/AsgardeoLoading.tsx rename to packages/nextjs/src/client/components/control/Loading/Loading.tsx index a42227f9..6a69d840 100644 --- a/packages/react/src/components/control/AsgardeoLoading.tsx +++ b/packages/nextjs/src/client/components/control/Loading/Loading.tsx @@ -16,13 +16,15 @@ * under the License. */ +'use client'; + import {FC, PropsWithChildren, ReactNode} from 'react'; -import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props for the Loading component. */ -export interface AsgardeoLoadingProps { +export interface LoadingProps { /** * Content to show when the user is not signed in. */ @@ -36,21 +38,18 @@ export interface AsgardeoLoadingProps { * * @example * ```tsx - * import { AsgardeoLoading } from '@asgardeo/auth-react'; + * import { Loading } from '@asgardeo/auth-react'; * * const App = () => { * return ( - * Finished Loading...

}> + * Finished Loading...

}> *

Loading...

- *
+ * * ); * } * ``` */ -const AsgardeoLoading: FC> = ({ - children, - fallback = null, -}: PropsWithChildren) => { +const Loading: FC> = ({children, fallback = null}: PropsWithChildren) => { const {isLoading} = useAsgardeo(); if (!isLoading) { @@ -60,6 +59,6 @@ const AsgardeoLoading: FC> = ({ return <>{children}; }; -AsgardeoLoading.displayName = 'AsgardeoLoading'; +Loading.displayName = 'Loading'; -export default AsgardeoLoading; +export default Loading; diff --git a/packages/react/package.json b/packages/react/package.json index 389e27ed..e129c4e8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -43,7 +43,7 @@ "devDependencies": { "@testing-library/dom": "^10.4.0", "@types/node": "^22.15.3", - "@types/react": "^19.1.4", + "@types/react": "^19.1.5", "@vitest/browser": "^3.1.3", "@wso2/eslint-plugin": "catalog:", "@wso2/prettier-config": "catalog:", @@ -63,9 +63,9 @@ }, "dependencies": { "@asgardeo/browser": "workspace:^", + "@emotion/css": "^11.13.5", "@floating-ui/react": "^0.27.12", "@types/react-dom": "^19.1.5", - "clsx": "^2.1.1", "esbuild": "^0.25.4", "react-dom": "^19.1.0", "tslib": "^2.8.1" diff --git a/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx b/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx index 0a09b290..8baf41ac 100644 --- a/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx +++ b/packages/react/src/components/actions/SignInButton/BaseSignInButton.tsx @@ -17,7 +17,7 @@ */ import {WithPreferences, withVendorCSSClassPrefix} from '@asgardeo/browser'; -import clsx from 'clsx'; +import {cx} from '@emotion/css'; import { ButtonHTMLAttributes, forwardRef, @@ -93,7 +93,7 @@ const BaseSignInButton: ForwardRefExoticComponent { + * return ( + * Finished Loading...

}> + *

Loading...

+ *
+ * ); + * } + * ``` + */ +const Loading: FC> = ({children, fallback = null}: PropsWithChildren) => { + const {isLoading} = useAsgardeo(); + + if (!isLoading) { + return <>{fallback}; + } + + return <>{children}; +}; + +Loading.displayName = 'Loading'; + +export default Loading; diff --git a/packages/react/src/components/control/SignedIn.tsx b/packages/react/src/components/control/SignedIn/SignedIn.tsx similarity index 96% rename from packages/react/src/components/control/SignedIn.tsx rename to packages/react/src/components/control/SignedIn/SignedIn.tsx index 76c9502c..2a656712 100644 --- a/packages/react/src/components/control/SignedIn.tsx +++ b/packages/react/src/components/control/SignedIn/SignedIn.tsx @@ -17,7 +17,7 @@ */ import {FC, PropsWithChildren, ReactNode} from 'react'; -import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props for the SignedIn component. diff --git a/packages/react/src/components/control/SignedOut.tsx b/packages/react/src/components/control/SignedOut/SignedOut.tsx similarity index 96% rename from packages/react/src/components/control/SignedOut.tsx rename to packages/react/src/components/control/SignedOut/SignedOut.tsx index af89c52b..cfad0b76 100644 --- a/packages/react/src/components/control/SignedOut.tsx +++ b/packages/react/src/components/control/SignedOut/SignedOut.tsx @@ -17,7 +17,7 @@ */ import {FC, PropsWithChildren, ReactNode} from 'react'; -import useAsgardeo from '../../contexts/Asgardeo/useAsgardeo'; +import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; /** * Props for the SignedOut component. diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts new file mode 100644 index 00000000..4928372a --- /dev/null +++ b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.styles.ts @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {css} from '@emotion/css'; +import {useMemo} from 'react'; +import {Theme} from '@asgardeo/browser'; + +/** + * Creates styles for the BaseCreateOrganization component using BEM methodology + * @param theme - The theme object containing design tokens + * @param colorScheme - The current color scheme (used for memoization) + * @returns Object containing CSS class names for component styling + */ +const useStyles = (theme: Theme, colorScheme: string) => { + return useMemo(() => { + const root = css` + padding: calc(${theme.vars.spacing.unit} * 4); + min-width: 600px; + margin: 0 auto; + `; + + const card = css` + background: ${theme.vars.colors.background.surface}; + border-radius: ${theme.vars.borderRadius.large}; + padding: calc(${theme.vars.spacing.unit} * 4); + `; + + const content = css` + display: flex; + flex-direction: column; + gap: calc(${theme.vars.spacing.unit} * 2); + `; + + const form = css` + display: flex; + flex-direction: column; + gap: calc(${theme.vars.spacing.unit} * 2); + width: 100%; + `; + + const header = css` + display: flex; + align-items: center; + gap: calc(${theme.vars.spacing.unit} * 1.5); + margin-bottom: calc(${theme.vars.spacing.unit} * 1.5); + `; + + const field = css` + display: flex; + align-items: center; + padding: ${theme.vars.spacing.unit} 0; + border-bottom: 1px solid ${theme.vars.colors.border}; + min-height: 32px; + `; + + const fieldGroup = css` + display: flex; + flex-direction: column; + gap: calc(${theme.vars.spacing.unit} * 0.5); + `; + + const textarea = css` + width: 100%; + padding: ${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 1.5); + border: 1px solid ${theme.vars.colors.border}; + border-radius: ${theme.vars.borderRadius.medium}; + font-size: ${theme.vars.typography.fontSizes.md}; + color: ${theme.vars.colors.text.primary}; + background-color: ${theme.vars.colors.background.surface}; + font-family: inherit; + min-height: 80px; + resize: vertical; + outline: none; + &:focus { + border-color: ${theme.vars.colors.primary.main}; + box-shadow: 0 0 0 2px ${theme.vars.colors.primary.main}20; + } + &:disabled { + background-color: ${theme.vars.colors.background.disabled}; + color: ${theme.vars.colors.text.secondary}; + cursor: not-allowed; + } + `; + + const textareaError = css` + border-color: ${theme.vars.colors.error.main}; + `; + + const input = css``; + + const avatarContainer = css` + align-items: flex-start; + display: flex; + gap: calc(${theme.vars.spacing.unit} * 2); + margin-bottom: ${theme.vars.spacing.unit}; + `; + + const actions = css` + display: flex; + gap: ${theme.vars.spacing.unit}; + justify-content: flex-end; + padding-top: calc(${theme.vars.spacing.unit} * 2); + `; + + const infoContainer = css` + display: flex; + flex-direction: column; + gap: ${theme.vars.spacing.unit}; + `; + + const value = css` + color: ${theme.vars.colors.text.primary}; + flex: 1; + display: flex; + align-items: center; + gap: ${theme.vars.spacing.unit}; + overflow: hidden; + min-height: 32px; + line-height: 32px; + `; + + const popup = css` + padding: calc(${theme.vars.spacing.unit} * 2); + `; + + const errorAlert = css` + margin-bottom: calc(${theme.vars.spacing.unit} * 2); + `; + + return { + root, + card, + content, + form, + header, + field, + fieldGroup, + textarea, + textareaError, + input, + avatarContainer, + actions, + infoContainer, + value, + popup, + errorAlert, + }; + }, [ + theme.vars.spacing.unit, + theme.vars.colors.background.surface, + theme.vars.colors.border, + theme.vars.borderRadius.large, + theme.vars.borderRadius.medium, + theme.vars.typography.fontSizes.md, + theme.vars.colors.text.primary, + theme.vars.colors.primary.main, + theme.vars.colors.background.disabled, + theme.vars.colors.text.secondary, + theme.vars.colors.error.main, + colorScheme, + ]); +}; + +export default useStyles; diff --git a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx index 30a6b194..fd5ed234 100644 --- a/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx +++ b/packages/react/src/components/presentation/CreateOrganization/BaseCreateOrganization.tsx @@ -16,114 +16,18 @@ * under the License. */ -import {withVendorCSSClassPrefix, CreateOrganizationPayload} from '@asgardeo/browser'; -import clsx from 'clsx'; -import {ChangeEvent, CSSProperties, FC, ReactElement, ReactNode, useMemo, useState} from 'react'; +import {CreateOrganizationPayload} from '@asgardeo/browser'; +import {cx} from '@emotion/css'; +import {ChangeEvent, CSSProperties, FC, ReactElement, ReactNode, useState} from 'react'; import useTheme from '../../../contexts/Theme/useTheme'; import useTranslation from '../../../hooks/useTranslation'; import Alert from '../../primitives/Alert/Alert'; import Button from '../../primitives/Button/Button'; -import {Dialog, DialogContent, DialogHeading} from '../../primitives/Popover/Popover'; +import Dialog from '../../primitives/Dialog/Dialog'; import FormControl from '../../primitives/FormControl/FormControl'; import InputLabel from '../../primitives/InputLabel/InputLabel'; import TextField from '../../primitives/TextField/TextField'; -import Typography from '../../primitives/Typography/Typography'; - -const useStyles = () => { - const {theme, colorScheme} = useTheme(); - - return useMemo( - () => ({ - root: { - padding: `calc(${theme.vars.spacing.unit} * 4)`, - minWidth: '600px', - margin: '0 auto', - } as CSSProperties, - card: { - background: theme.vars.colors.background.surface, - borderRadius: theme.vars.borderRadius.large, - padding: `calc(${theme.vars.spacing.unit} * 4)`, - } as CSSProperties, - content: { - display: 'flex', - flexDirection: 'column', - gap: `calc(${theme.vars.spacing.unit} * 2)`, - } as CSSProperties, - form: { - display: 'flex', - flexDirection: 'column', - gap: `calc(${theme.vars.spacing.unit} * 2)`, - width: '100%', - } as CSSProperties, - header: { - display: 'flex', - alignItems: 'center', - gap: `calc(${theme.vars.spacing.unit} * 1.5)`, - marginBottom: `calc(${theme.vars.spacing.unit} * 1.5)`, - } as CSSProperties, - field: { - display: 'flex', - alignItems: 'center', - padding: `${theme.vars.spacing.unit} 0`, - borderBottom: `1px solid ${theme.vars.colors.border}`, - minHeight: '32px', - } as CSSProperties, - textarea: { - width: '100%', - padding: `${theme.vars.spacing.unit} calc(${theme.vars.spacing.unit} * 1.5)`, - border: `1px solid ${theme.vars.colors.border}`, - borderRadius: theme.vars.borderRadius.medium, - fontSize: theme.vars.typography.fontSizes.md, - color: theme.vars.colors.text.primary, - backgroundColor: theme.vars.colors.background.surface, - fontFamily: 'inherit', - minHeight: '80px', - resize: 'vertical', - outline: 'none', - '&:focus': { - borderColor: theme.vars.colors.primary.main, - boxShadow: `0 0 0 2px ${theme.vars.colors.primary.main}20`, - }, - '&:disabled': { - backgroundColor: theme.vars.colors.background.disabled, - color: theme.vars.colors.text.secondary, - cursor: 'not-allowed', - }, - } as CSSProperties, - avatarContainer: { - alignItems: 'flex-start', - display: 'flex', - gap: `calc(${theme.vars.spacing.unit} * 2)`, - marginBottom: theme.vars.spacing.unit, - } as CSSProperties, - actions: { - display: 'flex', - gap: theme.vars.spacing.unit, - justifyContent: 'flex-end', - paddingTop: `calc(${theme.vars.spacing.unit} * 2)`, - } as CSSProperties, - infoContainer: { - display: 'flex', - flexDirection: 'column' as const, - gap: theme.vars.spacing.unit, - } as CSSProperties, - value: { - color: theme.vars.colors.text.primary, - flex: 1, - display: 'flex', - alignItems: 'center', - gap: theme.vars.spacing.unit, - overflow: 'hidden', - minHeight: '32px', - lineHeight: '32px', - } as CSSProperties, - popup: { - padding: `calc(${theme.vars.spacing.unit} * 2)`, - } as CSSProperties, - }), - [theme, colorScheme], - ); -}; +import useStyles from './BaseCreateOrganization.styles'; /** * Interface for organization form data. @@ -176,8 +80,8 @@ export const BaseCreateOrganization: FC = ({ style, title = 'Create Organization', }): ReactElement => { - const styles = useStyles(); - const {theme} = useTheme(); + const {theme, colorScheme} = useTheme(); + const styles = useStyles(theme, colorScheme); const {t} = useTranslation(); const [formData, setFormData] = useState({ description: '', @@ -214,7 +118,6 @@ export const BaseCreateOrganization: FC = ({ [field]: value, })); - // Clear error when user starts typing if (formErrors[field]) { setFormErrors(prev => ({ ...prev, @@ -223,23 +126,34 @@ export const BaseCreateOrganization: FC = ({ } }; + /** + * Handles changes to the organization name input. + * Automatically generates the organization handle based on the name if the handle is not set or matches + * + * @param value - The new value for the organization name. + */ const handleNameChange = (value: string): void => { handleInputChange('name', value); - // Auto-generate handle from name if handle is empty or matches previous auto-generated value if (!formData.handle || formData.handle === generateHandleFromName(formData.name)) { const newHandle = generateHandleFromName(value); handleInputChange('handle', newHandle); } }; + /** + * Removes special characters except space and hyphen from the organization name + * and generates a valid handle. + * @param name + * @returns + */ const generateHandleFromName = (name: string): string => { return name .toLowerCase() - .replace(/[^a-z0-9\s-]/g, '') // Remove special characters except spaces and hyphens - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen - .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); }; const handleSubmit = async (e: React.FormEvent): Promise => { @@ -268,33 +182,17 @@ export const BaseCreateOrganization: FC = ({ } }; - const containerStyle = { - ...styles.root, - ...(cardLayout ? styles.card : {}), - }; - const createOrganizationContent = ( -
-
-
- {/* Error Alert */} +
+
+ {error && ( - + Error {error} )} - - {/* Organization Name */} -
+
= ({ disabled={loading} required error={formErrors.name} - className={withVendorCSSClassPrefix('create-organization__input')} + className={cx(styles.input)} />
- - {/* Organization Handle */} -
+
= ({ required error={formErrors.handle} helperText="This will be your organization's unique identifier. Only lowercase letters, numbers, and hyphens are allowed." - className={withVendorCSSClassPrefix('create-organization__input')} + className={cx(styles.input)} />
- - {/* Organization Description */} -
+
{t('organization.create.description.label')}