Skip to content
Closed
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
66 changes: 66 additions & 0 deletions client/components/connect-screen/action-buttons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Button, Spinner } from '@wordpress/components';
import clsx from 'clsx';
import type { ActionButtonsProps } from './types';

import './style.scss';

/**
* Button group for connect screen actions
* @example
* <ActionButtons
* primaryLabel="Accept Invite"
* primaryOnClick={() => handleAccept()}
* primaryLoading={isSubmitting}
* secondaryLabel="Cancel"
* secondaryOnClick={() => handleCancel()}
* tertiaryLabel="Sign in with another account"
* tertiaryOnClick={() => handleSignIn()}
* />
*/
export function ActionButtons( {
primaryLabel,
primaryOnClick,
primaryLoading = false,
primaryDisabled = false,
secondaryLabel,
secondaryOnClick,
secondaryDisabled = false,
tertiaryLabel,
tertiaryOnClick,
className,
}: ActionButtonsProps ): JSX.Element {
return (
<div className={ clsx( 'connect-screen-action-buttons', className ) }>
<div className="connect-screen-action-buttons__main">
{ secondaryLabel && secondaryOnClick && (
<Button
variant="secondary"
onClick={ secondaryOnClick }
disabled={ secondaryDisabled || primaryLoading }
className="connect-screen-action-buttons__secondary"
>
{ secondaryLabel }
</Button>
) }
<Button
variant="primary"
onClick={ primaryOnClick }
disabled={ primaryDisabled || primaryLoading }
className="connect-screen-action-buttons__primary"
>
{ primaryLoading && <Spinner /> }
{ ! primaryLoading && primaryLabel }
</Button>
</div>
{ tertiaryLabel && tertiaryOnClick && (
<Button
variant="link"
onClick={ tertiaryOnClick }
className="connect-screen-action-buttons__tertiary"
>
{ tertiaryLabel }
</Button>
) }
</div>
);
}
66 changes: 66 additions & 0 deletions client/components/connect-screen/brand-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { isValidElement } from '@wordpress/element';
import clsx from 'clsx';
import type { BrandHeaderProps } from './types';

import './style.scss';

/**
* Brand header with logo, title, and optional description
* @example
* <BrandHeader
* logo="/path/to/logo.svg"
* logoAlt="Company Logo"
* logoWidth={72}
* logoHeight={24}
* title="Join the team"
* description="You've been invited to collaborate"
* />
* @example
* <BrandHeader
* logo={<CustomLogoComponent />}
* title="Connect your account"
* />
*/
export function BrandHeader( {
logo,
logoAlt = '',
logoWidth,
logoHeight,
title,
description,
className,
}: BrandHeaderProps ): JSX.Element {
const renderLogo = () => {
if ( ! logo ) {
return null;
}

if ( isValidElement( logo ) ) {
return <div className="connect-screen-brand-header__logo">{ logo }</div>;
}

if ( typeof logo === 'string' ) {
return (
<div className="connect-screen-brand-header__logo">
<img
src={ logo }
alt={ logoAlt }
width={ logoWidth }
height={ logoHeight }
className="connect-screen-brand-header__logo-image"
/>
</div>
);
}

return null;
};

return (
<div className={ clsx( 'connect-screen-brand-header', className ) }>
{ renderLogo() }
<h1 className="connect-screen-brand-header__title">{ title }</h1>
{ description && <p className="connect-screen-brand-header__description">{ description }</p> }
</div>
);
}
42 changes: 42 additions & 0 deletions client/components/connect-screen/consent-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createInterpolateElement, useMemo } from '@wordpress/element';
import clsx from 'clsx';
import type { ConsentTextProps } from './types';

import './style.scss';

/**
* Consent text with link support using createInterpolateElement
*
* Use XML-like tags in text to create links. Tag names should match keys in the links prop.
* @example
* <ConsentText
* text="By continuing, you agree to our <tosLink>Terms of Service</tosLink> and <privacyLink>Privacy Policy</privacyLink>."
* links={{
* tosLink: "https://wordpress.com/tos/",
* privacyLink: "https://automattic.com/privacy/"
* }}
* />
*/
export function ConsentText( { text, links = {}, className }: ConsentTextProps ): JSX.Element {
const interpolatedText = useMemo( () => {
// Build the interpolation options from links
const linkElements: Record< string, JSX.Element > = {};

for ( const [ key, url ] of Object.entries( links ) ) {
linkElements[ key ] = (
<a href={ url } target="_blank" rel="noopener noreferrer">
{ /* Placeholder content - will be replaced by createInterpolateElement */ }
</a>
);
}

// If no links, return plain text
if ( Object.keys( linkElements ).length === 0 ) {
return text;
}

return createInterpolateElement( text, linkElements );
}, [ text, links ] );

return <p className={ clsx( 'connect-screen-consent-text', className ) }>{ interpolatedText }</p>;
}
21 changes: 21 additions & 0 deletions client/components/connect-screen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Components
export { ScreenLayout } from './screen-layout';
export { BrandHeader } from './brand-header';
export { UserCard } from './user-card';
export { ActionButtons } from './action-buttons';
export { ConsentText } from './consent-text';
export { PermissionsList } from './permissions-list';
export { LoadingScreen } from './loading-screen';

// Types
export type {
UserCardUser,
Permission,
ScreenLayoutProps,
BrandHeaderProps,
UserCardProps,
ActionButtonsProps,
ConsentTextProps,
PermissionsListProps,
LoadingScreenProps,
} from './types';
19 changes: 19 additions & 0 deletions client/components/connect-screen/loading-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Spinner } from '@wordpress/components';
import clsx from 'clsx';
import type { LoadingScreenProps } from './types';

import './style.scss';

/**
* Full-screen loading state with spinner and optional message
* @example
* <LoadingScreen message="Connecting your account..." />
*/
export function LoadingScreen( { message, className }: LoadingScreenProps ): JSX.Element {
return (
<div className={ clsx( 'connect-screen-loading', className ) }>
<Spinner />
{ message && <p className="connect-screen-loading__message">{ message }</p> }
</div>
);
}
104 changes: 104 additions & 0 deletions client/components/connect-screen/permissions-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Button, Icon } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { check, seen, edit, cog, chevronDown } from '@wordpress/icons';
import clsx from 'clsx';
import { useTranslate } from 'i18n-calypso';
import type { PermissionsListProps, Permission } from './types';

import './style.scss';

const ICON_MAP = {
check,
view: seen,
edit,
manage: cog,
} as const;

/**
* Expandable permissions list with optional icons
* @example
* <PermissionsList
* title="This app will be able to:"
* permissions={[
* { icon: 'view', label: 'View your profile' },
* { icon: 'edit', label: 'Edit your posts' },
* { icon: 'manage', label: 'Manage settings' },
* ]}
* maxVisible={2}
* />
*/
export function PermissionsList( {
title,
permissions,
maxVisible = 4,
learnMoreText,
learnMoreUrl,
className,
}: PermissionsListProps ): JSX.Element {
const translate = useTranslate();
const [ isExpanded, setIsExpanded ] = useState( false );

const hasOverflow = permissions.length > maxVisible;
const visiblePermissions = isExpanded ? permissions : permissions.slice( 0, maxVisible );
const hiddenCount = permissions.length - maxVisible;

const renderIcon = ( permission: Permission ) => {
if ( ! permission.icon ) {
return <span className="connect-screen-permissions-list__icon-placeholder" />;
}

const icon = ICON_MAP[ permission.icon ];
return (
<span className="connect-screen-permissions-list__icon">
<Icon icon={ icon } size={ 20 } />
</span>
);
};

return (
<div className={ clsx( 'connect-screen-permissions-list', className ) }>
{ title && <h3 className="connect-screen-permissions-list__title">{ title }</h3> }
<ul className="connect-screen-permissions-list__items">
{ visiblePermissions.map( ( permission, index ) => (
<li key={ index } className="connect-screen-permissions-list__item">
{ renderIcon( permission ) }
<span className="connect-screen-permissions-list__label">{ permission.label }</span>
</li>
) ) }
</ul>
{ hasOverflow && ! isExpanded && (
<Button
variant="link"
onClick={ () => setIsExpanded( true ) }
className="connect-screen-permissions-list__expand"
>
{ translate( '%(count)d more', {
args: { count: hiddenCount },
comment: 'Button to show more permissions. e.g., "3 more"',
} ) }
<Icon icon={ chevronDown } size={ 20 } />
</Button>
) }
{ hasOverflow && isExpanded && (
<Button
variant="link"
onClick={ () => setIsExpanded( false ) }
className="connect-screen-permissions-list__collapse"
>
{ translate( 'Show less' ) }
<Icon icon={ chevronDown } size={ 20 } />
</Button>
) }
{ learnMoreText && learnMoreUrl && (
<Button
variant="link"
href={ learnMoreUrl }
target="_blank"
className="connect-screen-permissions-list__learn-more"
>
{ learnMoreText }
</Button>
) }
</div>
);
}
27 changes: 27 additions & 0 deletions client/components/connect-screen/screen-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import clsx from 'clsx';
import type { ScreenLayoutProps } from './types';

import './style.scss';

/**
* Full-screen centered layout for connect flows
* @example
* <ScreenLayout backgroundColor="#f6f7f7">
* <BrandHeader ... />
* <UserCard ... />
* <ActionButtons ... />
* </ScreenLayout>
*/
export function ScreenLayout( {
children,
className,
backgroundColor,
}: ScreenLayoutProps ): JSX.Element {
const style = backgroundColor ? { backgroundColor } : undefined;

return (
<div className={ clsx( 'connect-screen-layout', className ) } style={ style }>
<div className="connect-screen-layout__container">{ children }</div>
</div>
);
}
Loading