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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@internxt/ui",
"version": "0.0.26",
"version": "0.0.27",
"description": "Library of Internxt components",
"repository": {
"type": "git",
Expand Down Expand Up @@ -93,7 +93,7 @@
"storybook:build": "storybook build"
},
"dependencies": {
"@internxt/css-config": "1.0.3",
"@internxt/css-config": "1.0.5",
"@phosphor-icons/react": "^2.1.7",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/themes": "^3.2.0"
Expand Down
41 changes: 18 additions & 23 deletions src/components/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export interface DialogProps {
onSecondaryAction: () => void;
title: string;
subtitle: string;
primaryAction: string;
secondaryAction: string;
primaryAction: string | JSX.Element;
secondaryAction: string | JSX.Element;
primaryActionColor: 'primary' | 'danger';
isLoading?: boolean;
maxWidth?: 'sm' | 'md' | 'lg';
}

/**
Expand All @@ -36,18 +37,21 @@ export interface DialogProps {
* @property {string} subtitle
* - A subtitle for the dialog, displayed below the title.
*
* @property {string} primaryAction
* - The label for the primary action button.
* @property {string | JSX.Element} primaryAction
* - The label or content for the primary action button.
*
* @property {string} secondaryAction
* - The label for the secondary action button.
* - The label or content for the secondary action button.
*
* @property {('primary' | 'danger')} primaryActionColor
* - Defines the color of the primary action button. Can either be 'primary' or 'danger'.
*
* @property {boolean} [isLoading]
* - Optional flag to indicate if the buttons should show a loading state. Defaults to false.
*
* @property {'sm' | 'md' | 'lg'} [maxWidth]
* - Optional maximum width for the dialog. Can be 'sm', 'md', or 'lg'.
*
* @returns {JSX.Element}
* - The rendered dialog component.
*/
Expand All @@ -63,6 +67,7 @@ const Dialog = ({
secondaryAction,
primaryActionColor,
isLoading,
maxWidth = 'sm',
}: DialogProps): JSX.Element => {
const [isVisible, setIsVisible] = useState(isOpen);
const [transitionOpacity, setTransitionOpacity] = useState<string>('opacity-0');
Expand Down Expand Up @@ -107,29 +112,19 @@ const Dialog = ({
{isVisible && (
<div className={`fixed inset-0 z-50 ${isOpen ? '' : 'pointer-events-none'}`}>
<div
className={`absolute inset-0 bg-gray-100/50 transition-opacity
duration-150 dark:bg-black/75
${transitionOpacity}
`}
className={
`absolute inset-0 bg-gray-100/50 transition-opacity duration-150 ` +
`dark:bg-black/75 ${transitionOpacity}`
}
onClick={onClose}
data-testid="dialog-overlay"
></div>

<div
className={`absolute
left-1/2
top-1/2
w-full
max-w-sm
-translate-x-1/2
-translate-y-1/2
transform rounded-2xl
bg-surface p-5
transition-all
duration-150
dark:bg-gray-1
${transitionScale}
${transitionOpacity}`}
className={
`absolute left-1/2 top-1/2 w-full max-w-${maxWidth} -translate-x-1/2 -translate-y-1/2 transform ` +
`rounded-2xl bg-surface p-5 transition-all duration-150 dark:bg-gray-1 ${transitionScale} ${transitionOpacity}`
}
>
<div className="flex flex-col space-y-2">
<p className="text-2xl font-medium text-gray-100">{title}</p>
Expand Down
20 changes: 2 additions & 18 deletions src/components/dialog/__test__/__snapshots__/Dialog.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,11 @@ exports[`Dialog > should match snapshot 1`] = `
class="fixed inset-0 z-50 "
>
<div
class="absolute inset-0 bg-gray-100/50 transition-opacity
duration-150 dark:bg-black/75
opacity-0
"
class="absolute inset-0 bg-gray-100/50 transition-opacity duration-150 dark:bg-black/75 opacity-0"
data-testid="dialog-overlay"
/>
<div
class="absolute
left-1/2
top-1/2
w-full
max-w-sm
-translate-x-1/2
-translate-y-1/2
transform rounded-2xl
bg-surface p-5
transition-all
duration-150
dark:bg-gray-1
scale-95
opacity-0"
class="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 transform rounded-2xl bg-surface p-5 transition-all duration-150 dark:bg-gray-1 scale-95 opacity-0"
>
<div
class="flex flex-col space-y-2"
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from './popover';
export * from './radioButton';
export * from './skeletonLoader';
export * from './slider';
export * from './suiteLauncher';
export * from './switch';
export * from './table/Table';
export * from './textArea';
Expand Down
25 changes: 5 additions & 20 deletions src/components/popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,34 +77,19 @@ const Popover = ({ childrenButton, panel, className, classButton }: PopoverProps
<div style={{ lineHeight: 0 }} className={`relative ${className}`}>
<button
onClick={togglePopover}
onMouseDown={(e) => e.stopPropagation()}
className={`cursor-pointer outline-none ${classButton}`}
aria-expanded={isOpen}
data-testid="popover-button"
>
{childrenButton}
</button>
{showContent && (
<div
ref={panelRef}
className={`
absolute
right-0
z-50
mt-1
origin-top-right
transform
rounded-md
border
border-gray-10
bg-surface
py-1.5
shadow-subtle
duration-100
ease-out
dark:bg-gray-5
${transitionOpacity}
${transitionScale}
`}
className={
'absolute right-0 z-50 mt-1 origin-top-right transform rounded-md border border-gray-10 ' +
`bg-surface py-1.5 shadow-subtle duration-100 ease-out dark:bg-gray-5 ${transitionOpacity} ${transitionScale}`
}
>
{panel(closePopover)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exports[`Popover > should match snapshot 1`] = `
<button
aria-expanded="false"
class="cursor-pointer outline-none undefined"
data-testid="popover-button"
>
<span>
Open Popover
Expand All @@ -28,6 +29,7 @@ exports[`Popover > should match snapshot 1`] = `
<button
aria-expanded="false"
class="cursor-pointer outline-none undefined"
data-testid="popover-button"
>
<span>
Open Popover
Expand Down
107 changes: 107 additions & 0 deletions src/components/suiteLauncher/SuiteLauncher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { DotsNine, Lock } from '@phosphor-icons/react';
import { cloneElement, isValidElement } from 'react';
import { Popover } from '../popover';

export interface SuiteLauncherProps {
className?: string;
suiteArray: {
icon: JSX.Element;
title: string;
onClick: () => void;
isMain?: boolean;
availableSoon?: boolean;
isLocked?: boolean;
}[];
soonText?: string;
}

/**
* SuiteLauncher renders a dropdown menu with a list of suite applications.
*
* @param {suiteLauncherProps} props
* - Object containing properties for the suiteLauncher component.
*
* @param {suiteLauncherProps['suiteArray']} props.suiteArray
* - Array of objects containing the suite applications.
*
* @param {string} [props.className]
* - Optional CSS class name for the suiteLauncher component.
*
* @param {string} [props.soonText]
* - Optional text to display when a suite application is available soon. It should be a translated string. Defaults to "Soon".
*
* @returns {JSX.Element}
* - The rendered suiteLauncher component.
*/
export default function SuiteLauncher({
className = '',
suiteArray,
soonText,
}: Readonly<SuiteLauncherProps>): JSX.Element {
const SuiteButton = (
<div className="flex h-10 w-10 items-center justify-center text-black dark:text-white">
<DotsNine size={26} className="h-7 w-7" weight="bold" />
</div>
);

const panel = (
<div className="w-64 flex flex-wrap p-2" data-testid="suite-launcher-panel">
{suiteArray.map((suiteApp, idx) => (
<div
key={idx}
className={`w-1/3 flex items-center justify-center rounded-md ${suiteApp.isMain ? 'bg-primary/10 dark:bg-primary/20' : ''}`}
>
<div
role="none"
className={
`flex items-center px-3 py-2 text-gray-80 w-full rounded-md ` +
`${suiteApp.availableSoon ? '' : 'cursor-pointer hover:bg-gray-1 dark:hover:bg-gray-10'}`
}
style={{ lineHeight: 1.25 }}
onClick={suiteApp.availableSoon ? undefined : suiteApp.onClick}
>
<div className="flex flex-col items-center w-full rounded-md">
{suiteApp.isLocked ? (
<Lock size={26} weight="regular" data-testid="suite-launcher-lock-icon" />
) : isValidElement(suiteApp.icon as JSX.Element) ? (
cloneElement(suiteApp.icon as JSX.Element, {
size: 26,
className:
`${suiteApp.icon.props?.className ?? ''} ${suiteApp.isMain ? 'text-primary' : ''} ` +
`${suiteApp.availableSoon || suiteApp.isLocked ? 'opacity-50 filter grayscale' : ''}`,
weight: suiteApp.isMain ? 'fill' : 'regular',
})
Comment on lines +66 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice! 🚀

) : (
suiteApp.icon
)}

<div className="mt-1 flex items-center">
<span
className={`text-xs ${suiteApp.isMain ? 'text-primary font-medium' : 'text-gray-60'}`}
style={{ lineHeight: 1, opacity: suiteApp.availableSoon || suiteApp.isLocked ? 0.5 : 1 }}
>
{suiteApp.title}
</span>

{suiteApp.availableSoon && (
<div className="flex rounded-sm px-1 ml-1 py-0.5 bg-purple-1 dark:bg-purple-10 items-center">
<span
className="font-medium dark:font-normal text-purple-10 dark:text-purple-1"
style={{ lineHeight: 1, fontSize: 'xx-small' }}
>
{soonText ?? 'Soon'}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can pass the label as a prop😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It could be a good option, but for now I think its better this way so its easier to config (you only decide if its availableSoon, if its disabled or if its main, and ui just decides its css.
If its needed in the future, we can always add more complexity to it

</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
);

return (
<Popover className={className} childrenButton={SuiteButton} panel={() => panel} data-testid="app-suite-dropdown" />
);
}
57 changes: 57 additions & 0 deletions src/components/suiteLauncher/__test__/SuiteLauncher.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { render, screen, fireEvent } from '@testing-library/react';
import SuiteLauncher from '../SuiteLauncher';
import { describe, it, expect, vi } from 'vitest';

describe('SuiteLauncher', () => {
it('renders Popover button and panel', () => {
render(<SuiteLauncher suiteArray={[]} />);
expect(screen.getByTestId('popover-button')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('popover-button'));
expect(screen.getByTestId('suite-launcher-panel')).toBeInTheDocument();
});

it('renders suite items, shows soon text, renders lock icon and handles clicks correctly', () => {
const onClick1 = vi.fn();
const onClick2 = vi.fn();
const onClick3 = vi.fn();

const Icon = (props: any) => (
<span data-testid="mock-icon" {...props}>
Icon
</span>
);

const suiteArray = [
{ icon: <Icon className="orig" />, title: 'App 1', onClick: onClick1, isMain: true },
{ icon: <Icon />, title: 'App 2', onClick: onClick2, availableSoon: true },
{ icon: <Icon />, title: 'App 3', onClick: onClick3, isLocked: true },
];

render(<SuiteLauncher suiteArray={suiteArray} soonText="Pronto" />);
fireEvent.click(screen.getByTestId('popover-button'));

// Titles are rendered
expect(screen.getByText('App 1')).toBeInTheDocument();
expect(screen.getByText('App 2')).toBeInTheDocument();
expect(screen.getByText('App 3')).toBeInTheDocument();

// Custom soonText is shown for availableSoon items
expect(screen.getByText('Pronto')).toBeInTheDocument();

// Locked item shows the Lock icon
expect(screen.getByTestId('suite-launcher-lock-icon')).toBeInTheDocument();

// Clicking available item triggers its onClick
fireEvent.click(screen.getByText('App 1'));
expect(onClick1).toHaveBeenCalled();

// Clicking "availableSoon" item should NOT trigger its onClick
fireEvent.click(screen.getByText('App 2'));
expect(onClick2).not.toHaveBeenCalled();

// Even if locked, clicking should call its onClick because isLocked keeps the icon displayed
// the onClick handler is attached but opacity/grayscale is applied
fireEvent.click(screen.getByText('App 3'));
expect(onClick3).toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions src/components/suiteLauncher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as SuiteLauncher } from './SuiteLauncher';
export type { SuiteLauncherProps } from './SuiteLauncher';
14 changes: 4 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -551,13 +551,12 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==

"@internxt/css-config@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@internxt/css-config/-/css-config-1.0.3.tgz#16ca5d1559218173aa1befc78d6c0fe66481f309"
integrity sha512-Yc8gmShT6OJ537uE0IywUHVHzbGxeFtYcYKwcyUT8teoXZZcdZlcmqHahwNitST8Isc+cqeJ5HuWqtitsW3vGg==
"@internxt/css-config@1.0.5":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@internxt/css-config/-/css-config-1.0.5.tgz#ac320cb7c06c90a41b105e010040227a8747f54c"
integrity sha512-xA7sG+/OQiTq8Xf4KvBbazNwKsqThj8cBMQc1DslzjvhZl1H9BVXghyZGFIHTQ5rDRzE0GuZFu+IqO69heWyyQ==
dependencies:
tailwindcss "^3.4.14"
typescript "^5.6.3"

"@internxt/eslint-config-internxt@^1.0.10":
version "1.0.10"
Expand Down Expand Up @@ -6229,11 +6228,6 @@ typescript@5.4.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==

typescript@^5.6.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==

typescript@^5.7.3:
version "5.7.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
Expand Down