From 68701444d93251e2c4ee4257d4703717fabf2e80 Mon Sep 17 00:00:00 2001 From: Joanna S <37884374+JoannaSikora@users.noreply.github.com> Date: Fri, 31 Jan 2025 14:25:38 +0100 Subject: [PATCH 01/11] feat(UserGuide): update UserGuide component with new highlight functionality (#1487) Co-authored-by: Marcin Sawicki Co-authored-by: Marcin Sawicki --- .stylelintrc.json | 3 +- .../src/components/ActionBar/ActionBar.tsx | 1 + .../AnimatedTextContainer.mdx | 32 + .../AnimatedTextContainer.stories.tsx | 23 + .../AnimatedTextContainer.tsx | 50 ++ .../components/AnimatedTextContainer/index.ts | 1 + .../components/AnimatedTextContainer/types.ts | 22 + .../ExpirationCounter/ExpirationCounter.tsx | 1 + .../NavigationItem/NavigationItem.tsx | 1 + .../NavigationTopBar/NavigationTopBar.tsx | 5 +- .../components/NavigationTopBar/examples.tsx | 4 +- .../components/AppFrame/stories-helpers.tsx | 4 +- .../src/components/Tooltip/Tooltip.mdx | 95 --- .../components/Tooltip/Tooltip.stories.css | 27 - .../components/Tooltip/Tooltip.stories.tsx | 269 +------- .../components/UserGuide/SpotlightOverlay.tsx | 102 ---- .../components/UserGuide/UserGuide.tsx | 117 ---- .../components/UserGuide/UserGuideStep.tsx | 91 --- .../Tooltip/components/UserGuide/index.ts | 1 - .../components/Tooltip/components/index.ts | 1 - .../src/components/Tooltip/index.ts | 1 - .../src/components/UserGuide/UserGuide.mdx | 127 ++++ .../UserGuide/UserGuide.module.scss | 402 ++++++++++++ .../UserGuide/UserGuide.stories.css | 28 + .../UserGuide/UserGuide.stories.tsx | 573 ++++++++++++++++++ .../src/components/UserGuide/UserGuide.tsx | 191 ++++++ .../UserGuideBubbleStep.module.scss | 118 ++++ .../UserGuideBubbleStep.spec.tsx | 45 ++ .../UserGuideBubbleStep.tsx | 114 ++++ .../components/UserGuideBubbleStep/types.ts | 32 + .../UserGuideStep/UserGuideStep.module.scss | 62 ++ .../UserGuideStep/UserGuideStep.spec.tsx | 77 +++ .../UserGuideStep/UserGuideStep.tsx | 111 ++++ .../components/UserGuideStep/types.ts | 50 ++ .../components/UserGuide/components/index.ts | 2 + .../src/components/UserGuide/index.ts | 3 + .../src/components/UserGuide/placeholder.png | Bin 0 -> 5381 bytes .../components/UserGuide/stories-helpers.css | 84 +++ .../components/UserGuide/stories-helpers.tsx | 84 +++ .../UserGuide/styles/transitions.scss | 17 + .../src/components/UserGuide/types.ts | 44 ++ .../UserGuide/virtualElementReference.ts | 8 +- .../src/foundations/shadow.css | 12 + .../src/foundations/transition.css | 6 + packages/react-components/src/index.ts | 1 + .../src/stories/assets/cursor.svg | 24 + packages/react-components/src/utils/types.ts | 4 + 47 files changed, 2359 insertions(+), 711 deletions(-) create mode 100644 packages/react-components/src/components/AnimatedTextContainer/AnimatedTextContainer.mdx create mode 100644 packages/react-components/src/components/AnimatedTextContainer/AnimatedTextContainer.stories.tsx create mode 100644 packages/react-components/src/components/AnimatedTextContainer/AnimatedTextContainer.tsx create mode 100644 packages/react-components/src/components/AnimatedTextContainer/index.ts create mode 100644 packages/react-components/src/components/AnimatedTextContainer/types.ts delete mode 100644 packages/react-components/src/components/Tooltip/components/UserGuide/SpotlightOverlay.tsx delete mode 100644 packages/react-components/src/components/Tooltip/components/UserGuide/UserGuide.tsx delete mode 100644 packages/react-components/src/components/Tooltip/components/UserGuide/UserGuideStep.tsx delete mode 100644 packages/react-components/src/components/Tooltip/components/UserGuide/index.ts create mode 100644 packages/react-components/src/components/UserGuide/UserGuide.mdx create mode 100644 packages/react-components/src/components/UserGuide/UserGuide.module.scss create mode 100644 packages/react-components/src/components/UserGuide/UserGuide.stories.css create mode 100644 packages/react-components/src/components/UserGuide/UserGuide.stories.tsx create mode 100644 packages/react-components/src/components/UserGuide/UserGuide.tsx create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.module.scss create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.spec.tsx create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.tsx create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/types.ts create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.module.scss create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.spec.tsx create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.tsx create mode 100644 packages/react-components/src/components/UserGuide/components/UserGuideStep/types.ts create mode 100644 packages/react-components/src/components/UserGuide/components/index.ts create mode 100644 packages/react-components/src/components/UserGuide/index.ts create mode 100644 packages/react-components/src/components/UserGuide/placeholder.png create mode 100644 packages/react-components/src/components/UserGuide/stories-helpers.css create mode 100644 packages/react-components/src/components/UserGuide/stories-helpers.tsx create mode 100644 packages/react-components/src/components/UserGuide/styles/transitions.scss create mode 100644 packages/react-components/src/components/UserGuide/types.ts rename packages/react-components/src/components/{Tooltip/components => }/UserGuide/virtualElementReference.ts (77%) create mode 100644 packages/react-components/src/stories/assets/cursor.svg diff --git a/.stylelintrc.json b/.stylelintrc.json index 92f0c9482..3c6cbc4c6 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -9,6 +9,7 @@ "selector-class-pattern": null, "no-descending-specificity": null, "order/properties-alphabetical-order": null, - "import-notation": "string" + "import-notation": "string", + "no-invalid-position-at-import-rule": null } } diff --git a/packages/react-components/src/components/ActionBar/ActionBar.tsx b/packages/react-components/src/components/ActionBar/ActionBar.tsx index f8cb01552..afac62116 100644 --- a/packages/react-components/src/components/ActionBar/ActionBar.tsx +++ b/packages/react-components/src/components/ActionBar/ActionBar.tsx @@ -135,6 +135,7 @@ export const ActionBar: React.FC = ({ )} triggerRenderer={ -
-
dispatch({ type: 'reference1' })} - id="reference1" - className="guide-reference" - > - Example reference 1 -
-
dispatch({ type: 'reference2' })} - id="reference2" - className="guide-reference" - > - Example reference 2 -
- -
dispatch({ type: 'reference3' })} - id="reference3" - className="guide-reference" - > - Example reference 3 -
- - - {state.reference === 'reference1' && } - {state.reference === 'reference2' && } - {state.reference === 'reference3' && } - -
- -); - -``` - - - ## Component API
diff --git a/packages/react-components/src/components/Tooltip/Tooltip.stories.css b/packages/react-components/src/components/Tooltip/Tooltip.stories.css index ae63d4bd4..2007881f8 100644 --- a/packages/react-components/src/components/Tooltip/Tooltip.stories.css +++ b/packages/react-components/src/components/Tooltip/Tooltip.stories.css @@ -19,33 +19,6 @@ height: 800px; } -.simple-user-guide-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 20px; -} - -.guide-container { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 20px; -} - -.guide-reference { - display: flex; - align-items: center; - justify-content: center; - border: 1px dashed var(--border-default); - border-radius: 4px; - background-color: var(--surface-basic-default); - font-size: 18px; - padding: 10px; -} - .tooltip-preview-reports { display: flex; align-items: center; diff --git a/packages/react-components/src/components/Tooltip/Tooltip.stories.tsx b/packages/react-components/src/components/Tooltip/Tooltip.stories.tsx index 25e309ba3..7c6033fcb 100644 --- a/packages/react-components/src/components/Tooltip/Tooltip.stories.tsx +++ b/packages/react-components/src/components/Tooltip/Tooltip.stories.tsx @@ -8,8 +8,7 @@ import noop from '../../utils/noop'; import { Button } from '../Button'; import './Tooltip.stories.css'; -import { Info, Interactive, Reports, Simple, UserGuide } from './components'; -import { UserGuideStep } from './components/UserGuide/UserGuideStep'; +import { Info, Interactive, Reports, Simple } from './components'; import beautifulImage from './placeholder.png'; import { Tooltip } from './Tooltip'; import { ITooltipProps } from './types'; @@ -57,7 +56,6 @@ export default { Interactive, Reports, Simple, - UserGuide, }, } as Meta; @@ -180,268 +178,3 @@ TooltipReports.decorators = [ ), ]; - -export const SimpleUserGuide = (): React.ReactElement => { - const reducer = ( - state: { isVisible: boolean; reference: string }, - action: { type: string } - ) => { - if (action.type === 'reference1') { - return { - ...state, - reference: 'reference1', - }; - } - if (action.type === 'reference2') { - return { - ...state, - reference: 'reference2', - }; - } - if (action.type === 'reference3') { - return { - ...state, - reference: 'reference3', - }; - } - if (action.type === 'isVisible') { - return { - reference: 'reference1', - isVisible: !state.isVisible, - }; - } - - return state; - }; - - const [state, dispatch] = React.useReducer(reducer, { - reference: 'reference1', - isVisible: false, - }); - - return ( -
- -
-
dispatch({ type: 'reference1' })} - id="reference1" - className="guide-reference" - > - Example reference 1 -
-
dispatch({ type: 'reference2' })} - id="reference2" - className="guide-reference" - > - Example reference 2 -
- -
dispatch({ type: 'reference3' })} - id="reference3" - className="guide-reference" - > - Example reference 3 -
- - - {state.reference === 'reference1' ? ( - - ) : null} - - {state.reference === 'reference2' ? ( - - ) : null} - - {state.reference === 'reference3' ? ( - - ) : null} - -
-
- ); -}; - -export const ExtendedContentUserGuide = ( - args: ITooltipProps -): React.ReactElement => ( -
- -
-); - -ExtendedContentUserGuide.args = { - placement: 'bottom', - isVisible: true, - theme: 'default', - triggerOnClick: false, - arrowOffsetY: 0, - arrowOffsetX: 0, - offsetMainAxis: 8, - withFadeAnimation: true, - transitionDuration: 200, - transitionDelay: 0, - hoverOutDelayTimeout: 100, -}; - -const TooltipUserGuideExample: React.FC = (props) => { - const reducer = ( - state: { isVisible: boolean; reference: string }, - action: { type: string } - ) => { - if (action.type === 'reference1') { - return { - ...state, - reference: 'reference1', - }; - } - if (action.type === 'reference2') { - return { - ...state, - reference: 'reference2', - }; - } - if (action.type === 'reference3') { - return { - ...state, - reference: 'reference3', - }; - } - if (action.type === 'isVisible') { - return { - reference: 'reference1', - isVisible: !state.isVisible, - }; - } - - return state; - }; - - const [state, dispatch] = React.useReducer(reducer, { - reference: 'reference1', - isVisible: false, - }); - - return ( -
- -
-
dispatch({ type: 'reference1' })} - id="reference1" - style={{ - display: 'block', - backgroundColor: 'red', - height: '50px', - width: '100px', - }} - >
-
dispatch({ type: 'reference2' })} - id="reference2" - style={{ - display: 'block', - backgroundColor: 'red', - height: '50px', - width: '100px', - alignSelf: 'flex-start', - }} - >
- -
dispatch({ type: 'reference3' })} - id="reference3" - style={{ - display: 'block', - backgroundColor: 'red', - height: '50px', - width: '100px', - }} - >
- - - {state.reference === 'reference1' ? ( - dispatch({ type: 'reference2' })} - handleCloseAction={() => dispatch({ type: 'isVisible' })} - currentStep={1} - stepMax={3} - closeWithX - /> - ) : null} - - {state.reference === 'reference2' ? ( - dispatch({ type: 'reference3' })} - handleCloseAction={() => dispatch({ type: 'isVisible' })} - currentStep={2} - stepMax={3} - closeWithX - /> - ) : null} - - {state.reference === 'reference3' ? ( - dispatch({ type: 'isVisible' })} - handleCloseAction={() => { - dispatch({ type: 'isVisible' }); - }} - currentStep={3} - stepMax={3} - closeWithX - /> - ) : null} - -
-
- ); -}; diff --git a/packages/react-components/src/components/Tooltip/components/UserGuide/SpotlightOverlay.tsx b/packages/react-components/src/components/Tooltip/components/UserGuide/SpotlightOverlay.tsx deleted file mode 100644 index 09383ffb7..000000000 --- a/packages/react-components/src/components/Tooltip/components/UserGuide/SpotlightOverlay.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from 'react'; - -import cx from 'clsx'; - -import styles from '../../Tooltip.module.scss'; - -const baseClass = 'guide-tooltip'; - -const SpotlightOverlay = ({ - gap, - isVisible, - slide, - disablePointerEvents, -}: { - gap: DOMRect | null; - isVisible: boolean; - slide: boolean; - disablePointerEvents: boolean; -}): React.ReactElement | null => { - if (!gap) return null; - const overlayLeft = { - top: `${gap.top}px`, - left: '0', - width: `${gap.left}px`, - height: `${gap.height}px`, - }; - const overlayRight = { - top: `${gap.top}px`, - left: `${gap.right}px`, - width: `calc(100% - ${gap.right}px)`, - height: `${gap.height}px`, - }; - const overlayTop = { - top: '0', - left: '0', - width: '100%', - height: `${gap.top}px`, - }; - const overlayBottom = { - top: `${gap.bottom}px`, - left: '0', - width: '100%', - height: `calc(100% - ${gap.bottom}px)`, - }; - - const spotlight = { - top: `${gap.top}px`, - left: `${gap.left}px`, - width: `${gap.width}px`, - height: `${gap.height}px`, - backgroundColor: 'transparent', - }; - - return ( - <> -
-
-
-
- {disablePointerEvents && ( -
- )} - - ); -}; - -export default SpotlightOverlay; diff --git a/packages/react-components/src/components/Tooltip/components/UserGuide/UserGuide.tsx b/packages/react-components/src/components/Tooltip/components/UserGuide/UserGuide.tsx deleted file mode 100644 index 5d97623c7..000000000 --- a/packages/react-components/src/components/Tooltip/components/UserGuide/UserGuide.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from 'react'; - -import { FloatingPortal } from '@floating-ui/react'; -import cx from 'clsx'; - -import { ModalPortalProps } from '../../../Modal'; -import { Tooltip } from '../../Tooltip'; -import styles from '../../Tooltip.module.scss'; -import { ITooltipProps } from '../../types'; - -import SpotlightOverlay from './SpotlightOverlay'; -import VirtualReference from './virtualElementReference'; - -const spotlightPadding = 8; -const baseClass = 'guide-tooltip'; - -const virtualReference = (element: Element, padding: number) => - new VirtualReference(element, padding); - -interface IOwnProps { - shouldSlide?: boolean; - className?: string; - disableSpotlightPointerEvents?: boolean; -} - -interface IUserGuide - extends IOwnProps, - Omit, - Omit {} - -export const UserGuide: React.FC> = ( - props -) => { - const { - className, - parentElementName, - isVisible = false, - shouldSlide = true, - } = props; - - const [parentElement, setParentElement] = React.useState( - null - ); - - const [rect, setRect] = React.useState(null); - const [isSliding, setIsSliding] = React.useState(shouldSlide); - - const handleViewportChange = () => { - if (parentElement) { - setRect( - virtualReference( - parentElement, - spotlightPadding - ).getBoundingClientRect() as DOMRect - ); - setIsSliding(false); - } - }; - - React.useEffect(() => { - if (parentElement !== null) { - window.addEventListener('resize', handleViewportChange); - window.addEventListener('scroll', handleViewportChange); - - return () => { - window.removeEventListener('resize', handleViewportChange); - window.removeEventListener('scroll', handleViewportChange); - }; - } - }, [parentElement]); - - React.useEffect(() => { - if (parentElementName) { - const element = document.querySelector(parentElementName); - setParentElement(element); - } - }, [parentElementName]); - - React.useEffect(() => { - parentElement && - setRect( - virtualReference( - parentElement, - spotlightPadding - ).getBoundingClientRect() as DOMRect - ); - setIsSliding(true); - }, [parentElement]); - - return parentElement && isVisible && rect ? ( - - - } - referenceElement={{ - getBoundingClientRect: () => { - return rect; - }, - contextElement: parentElement, - }} - className={cx({ - [styles[baseClass]]: true, - [styles[`${baseClass}--slide`]]: isSliding, - className: className, - })} - > - {props.children} - - - ) : null; -}; diff --git a/packages/react-components/src/components/Tooltip/components/UserGuide/UserGuideStep.tsx b/packages/react-components/src/components/Tooltip/components/UserGuide/UserGuideStep.tsx deleted file mode 100644 index 20024a6e4..000000000 --- a/packages/react-components/src/components/Tooltip/components/UserGuide/UserGuideStep.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; - -import { Close } from '@livechat/design-system-icons'; -import cx from 'clsx'; - -import { Button } from '../../../Button'; -import { Icon } from '../../../Icon'; -import { Heading } from '../../../Typography'; -import { getIconType } from '../../Tooltip.helpers'; -import styles from '../../Tooltip.module.scss'; -import { TooltipTheme } from '../../types'; - -const baseClass = 'tooltip'; - -export const UserGuideStep: React.FC<{ - header: string; - text: string; - image?: { - src: string; - alt: string; - }; - currentStep: number; - stepMax: number; - closeWithX?: boolean; - theme?: TooltipTheme; - handleClickPrimary: () => void; - handleCloseAction?: (ev: KeyboardEvent | React.MouseEvent) => void; -}> = ({ - header, - text, - image, - currentStep, - stepMax, - closeWithX, - theme, - handleCloseAction, - handleClickPrimary, -}) => { - React.useEffect(() => { - if (handleCloseAction) { - document.addEventListener('keydown', handleCloseAction); - - return () => { - document.removeEventListener('keydown', handleCloseAction); - }; - } - }, []); - - return ( -
- {closeWithX && ( - - )} - {image && ( -
- {image.alt} -
- )} - {header && ( - - {header} - - )} -
{text}
-
- - Step {currentStep} of {stepMax} - - -
-
- ); -}; diff --git a/packages/react-components/src/components/Tooltip/components/UserGuide/index.ts b/packages/react-components/src/components/Tooltip/components/UserGuide/index.ts deleted file mode 100644 index 1db00e622..000000000 --- a/packages/react-components/src/components/Tooltip/components/UserGuide/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UserGuide } from './UserGuide'; diff --git a/packages/react-components/src/components/Tooltip/components/index.ts b/packages/react-components/src/components/Tooltip/components/index.ts index e7afcdf97..77b9b004a 100644 --- a/packages/react-components/src/components/Tooltip/components/index.ts +++ b/packages/react-components/src/components/Tooltip/components/index.ts @@ -1,5 +1,4 @@ export { Simple } from './Simple'; export { Info } from './Info'; export { Interactive } from './Interactive'; -export { UserGuide } from './UserGuide'; export { Reports } from './Reports'; diff --git a/packages/react-components/src/components/Tooltip/index.ts b/packages/react-components/src/components/Tooltip/index.ts index d6937a2ad..0876c6d28 100644 --- a/packages/react-components/src/components/Tooltip/index.ts +++ b/packages/react-components/src/components/Tooltip/index.ts @@ -1,6 +1,5 @@ export { Tooltip } from './Tooltip'; export { Simple, Info, Interactive, Reports } from './components'; -export { UserGuide } from './components/UserGuide'; export type { ITooltipProps, ITooltipInfoProps, diff --git a/packages/react-components/src/components/UserGuide/UserGuide.mdx b/packages/react-components/src/components/UserGuide/UserGuide.mdx new file mode 100644 index 000000000..cbd07d592 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/UserGuide.mdx @@ -0,0 +1,127 @@ +import { Meta, Title, Canvas, ArgTypes } from '@storybook/blocks'; + +import * as Stories from './UserGuide.stories'; + + + +UserGuide + +[Intro](#Intro) | [Usage](#Usage) | [Style highlighted element](#style) | [UserGuideStep](#UserGuideStep) | [UserGuideBubbleStep](#UserGuideBubbleStep) | [Component API](#ComponentAPI) | [Content Spec](#ContentSpec) + +## Intro
+ +The `UserGuide` component provides a guided tour functionality to help users navigate through different features of your application. It enhances user experience by offering step-by-step guidance in an interactive way. + +## Usage + +Here’s an example of how you can integrate the `UserGuide` component into your application: + +```tsx +import { + UserGuide, + UserGuideStep, +} from '@livechat/design-system-react-components'; + +return ( +
+
+ Example reference 1 +
+
+ Example reference 2 +
+
+ Example reference 3 +
+ + + {/* here you should put the `UserGuideStep` or `UserGuideBubbleStep` components */} + + + +
+); +``` + +The above example only shows the way of implementation, state management is on the implementation side. +You can manage it in any way, the most important is to place in `UserGuide` an element that is to be +displayed next to the element the guide points to. We suggest using `UserGuideStep`. + +#### Important + +- `parentElementName` - it requires the correct id of the element which should be highlighted, the element should be visible in the UI + +## Style highlighted element
+ +Component allows to easily style the highlighted element, by passing own styles properties. To do this, you need to enchant the `UserGuide` component with styles object using the `elementStyles` prop: + +```tsx + +``` + +You can use your state managment to put different styles for highliughted elements, the component will update it. + +## UserGuideStep + +`UserGuideStep` is the component which should be used as the steps of the user guide. Used inside the `UserGuide` it will be positioned with the cursor based on the `cursorPosition`. + + + +```tsx + +``` + +## UserGuideBubbleStep + +`UserGuideBubbleStep` is a variant of the guide step that displays content in a bubble format with celebration animations. Used inside the `UserGuide`, it provides a different visual style for highlighting features. + + + +```tsx +} + message="We have prepared the playground ready for you to test all capabilities of chat section. If you have any question, just trigger me from the upper -right corner of the screen!" + cta={/* your custom cta buttons */} +/> +``` + +## Component API + + + +## Content Spec + + + Go to Figma documentation + \ No newline at end of file diff --git a/packages/react-components/src/components/UserGuide/UserGuide.module.scss b/packages/react-components/src/components/UserGuide/UserGuide.module.scss new file mode 100644 index 000000000..338e15d70 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/UserGuide.module.scss @@ -0,0 +1,402 @@ +@use './styles/transitions'; + +@import '../../utils/StackingContextLevel'; + +$base-class: 'user-guide'; +$radius: var(--radius-3); +$arrow-size: 45px; + +$animations: ( + 'right-start': fade-in-right-start, + 'right': fade-in-right, + 'right-end': fade-in-right-end, + 'left-start': fade-in-left-start, + 'left': fade-in-left, + 'left-end': fade-in-left-end, + 'top-start': fade-in-top-start, + 'top': fade-in-top, + 'top-end': fade-in-top-end, + 'bottom-start': fade-in-bottom-start, + 'bottom': fade-in-bottom, + 'bottom-end': fade-in-bottom-end +); + +.#{$base-class} { + &__overlay { + position: fixed; + inset: 0; + background-color: var(--surface-other-overlay); + z-index: $stacking-context-level-tooltip; + animation: fade-in var(--transition-duration-moderate-2) forwards; + backdrop-filter: blur(2px); + display: flex; + justify-content: center; + align-items: center; + + &__content { + opacity: 0; + animation-delay: var(--transition-duration-moderate-2); + animation-duration: var(--transition-duration-moderate-2); + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + animation-name: fade-in; + } + } + + &__floating { + @include transitions.durations(); + + transition-property: top, bottom, left, right; + transition-timing-function: ease-in-out; + z-index: $stacking-context-level-tooltip; + opacity: 0; + animation-duration: var(--transition-duration-moderate-2); + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + animation-name: fade-in; + } + + &__guide { + display: flex; + position: relative; + height: 0px; + width: 0px; + + &--right-start, + &--left-start { + align-items: flex-start; + } + + &--right, + &--left { + align-items: center; + } + + &--right-end, + &--left-end { + align-items: flex-end + } + + &--bottom-start, + &--top-start { + justify-content: flex-start; + } + + &--bottom, + &--top { + justify-content: center; + } + + &--bottom-end, + &--top-end { + justify-content: flex-end + } + + &--top-start, + &--top, + &--top-end { + align-items: flex-end; + } + + &--left-start, + &--left, + &--left-end { + justify-content: flex-end; + } + + &__arrow { + @include transitions.durations(); + + display: flex; + position: absolute; + top: 0; + left: 0; + transition-property: top, left, transform; + transition-timing-function: ease-in-out; + + &--right-start { + top: 9px; + transform: rotate(-50deg); + } + + &--right { + top: calc(50% - #{$arrow-size / 2}); + transform: rotate(-90deg); + } + + &--right-end { + top: calc(100% - $arrow-size - 9px); + transform: rotate(-130deg) + } + + &--bottom-start, + &--bottom, + &--bottom-end { + top: 0px; + } + + &--bottom-start { + left: 9px; + transform: rotate(-40deg); + } + + &--bottom { + left: calc(50% - #{$arrow-size / 2}); + transform: rotate(0deg); + } + + &--bottom-end { + left: calc(100% - #{$arrow-size} - 9px); + transform: rotate(40deg); + } + + &--top-start, + &--top, + &--top-end { + top: calc(100% - #{$arrow-size}); + } + + &--top-start { + left: 9px; + transform: rotate(-140deg); + } + + &--top { + left: calc(50% - #{$arrow-size / 2}); + transform: rotate(-180deg); + } + + &--top-end { + left: calc(100% - #{$arrow-size} - 9px); + transform: rotate(140deg); + } + + &--left-start, + &--left, + &--left-end { + left: calc(100% - 43px); + } + + &--left-start { + top: 9px; + transform: rotate(50deg); + } + + &--left { + top: calc(50% - #{$arrow-size / 2}); + left: calc(100% - $arrow-size); + transform: rotate(90deg); + } + + &--left-end { + top: calc(100% - $arrow-size - 9px); + transform: rotate(130deg); + } + } + + &__content { + margin: 42px; + opacity: 0; + animation-delay: var(--transition-duration-moderate-2); + animation-duration: var(--transition-duration-moderate-2); + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + + @each $key, $animation-name in $animations { + &--#{$key} { + animation-name: $animation-name; + } + } + } + } + + &__cursor { + transition: transform ease-in-out 0.2s; + + &--bottom { + transform: rotate(0deg); + } + + &--left { + transform: rotate(-45deg); + } + + &--right { + transform: rotate(45deg); + } + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fade-in-right-start { + 0% { + opacity: 0; + margin-left: 25px; + margin-top: 25px; + } + + 100% { + opacity: 1; + margin-left: 42px; + margin-top: 42px; + } +} + +@keyframes fade-in-right { + 0% { + opacity: 0; + margin-left: 25px; + } + + 100% { + opacity: 1; + margin-left: 42px; + } +} + +@keyframes fade-in-right-end { + 0% { + opacity: 0; + margin-left: 25px; + margin-bottom: 25px; + } + + 100% { + opacity: 1; + margin-left: 42px; + margin-bottom: 42px; + } +} + +@keyframes fade-in-left-start { + 0% { + opacity: 0; + margin-right: 25px; + margin-top: 25px; + } + + 100% { + opacity: 1; + margin-right: 42px; + margin-top: 42px; + } +} + +@keyframes fade-in-left { + 0% { + opacity: 0; + margin-right: 25px; + } + + 100% { + opacity: 1; + margin-right: 42px; + } +} + +@keyframes fade-in-left-end { + 0% { + opacity: 0; + margin-right: 25px; + margin-bottom: 25px; + } + + 100% { + opacity: 1; + margin-right: 42px; + margin-bottom: 42px; + } +} + +@keyframes fade-in-top-start { + 0% { + opacity: 0; + margin-left: 25px; + margin-bottom: 25px; + } + + 100% { + opacity: 1; + margin-left: 42px; + margin-bottom: 42px; + } +} + +@keyframes fade-in-top { + 0% { + opacity: 0; + margin-bottom: 25px; + } + + 100% { + opacity: 1; + margin-bottom: 42px; + } +} + +@keyframes fade-in-top-end { + 0% { + opacity: 0; + margin-right: 25px; + margin-bottom: 25px; + } + + 100% { + opacity: 1; + margin-right: 42px; + margin-bottom: 42px; + } +} + +@keyframes fade-in-bottom-start { + 0% { + opacity: 0; + margin-left: 25px; + margin-top: 25px; + } + + 100% { + opacity: 1; + margin-left: 42px; + margin-top: 42px; + } +} + +@keyframes fade-in-bottom { + 0% { + opacity: 0; + margin-top: 25px; + } + + 100% { + opacity: 1; + margin-top: 42px; + } +} + +@keyframes fade-in-bottom-end { + 0% { + opacity: 0; + margin-right: 25px; + margin-top: 25px; + } + + 100% { + opacity: 1; + margin-right: 42px; + margin-top: 42px; + } +} + +:global(.user-guide-visible) { + overflow: hidden !important; +} diff --git a/packages/react-components/src/components/UserGuide/UserGuide.stories.css b/packages/react-components/src/components/UserGuide/UserGuide.stories.css new file mode 100644 index 000000000..4533224bd --- /dev/null +++ b/packages/react-components/src/components/UserGuide/UserGuide.stories.css @@ -0,0 +1,28 @@ +.preview-container { + display: flex; + align-items: center; + justify-content: center; + height: 800px; +} + +.simple-user-guide-container { + display: flex; + flex-direction: column; + gap: 20px; + align-items: center; + justify-content: center; +} + +.guide-reference { + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed var(--border-default); + border-radius: 4px; + padding: 10px; + font-size: 18px; +} + +.gap { + gap: 10px; +} diff --git a/packages/react-components/src/components/UserGuide/UserGuide.stories.tsx b/packages/react-components/src/components/UserGuide/UserGuide.stories.tsx new file mode 100644 index 000000000..ea5cb0ce4 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/UserGuide.stories.tsx @@ -0,0 +1,573 @@ +import { CSSProperties, ReactElement, useReducer, useState } from 'react'; + +import { Placement } from '@floating-ui/react'; +import * as Icons from '@livechat/design-system-icons'; +import { Meta } from '@storybook/react'; + +import { AppFrame } from '../AppFrame'; +import { + Navigation, + NavigationItem, + NavigationGroup, + ExpirationCounter, + MobileNavigation, +} from '../AppFrame/components'; +import { + ExampleTopBar, + getArchivesSubMenu, + getBadgeContent, + getChatsMenu, + getEngageSubMenu, +} from '../AppFrame/stories-helpers'; +import { Avatar } from '../Avatar'; +import { Button } from '../Button'; +import { Icon } from '../Icon'; +import { ProductSwitcher, useProductSwitcher } from '../ProductSwitcher'; +import { Tooltip } from '../Tooltip'; + +import { UserGuideBubbleStep, UserGuideStep } from './components'; +import { AppContent } from './stories-helpers'; +import { CursorTiming } from './types'; +import { UserGuide } from './UserGuide'; + +import './UserGuide.stories.css'; + +export default { + title: 'Components/UserGuide', + component: UserGuide, + argTypes: { + triggerRenderer: { + control: false, + }, + useDismissHookProps: { + control: false, + }, + }, + parameters: { + controls: { expanded: true }, + chromatic: { delay: 300 }, + layout: 'fullscreen', + }, + subcomponents: { UserGuideStep, UserGuideBubbleStep }, +} as Meta; + +const navigationItems = [ + 'home', + 'chats', + 'engage', + 'archives', + 'tickets', + 'team', + 'reports', + 'apps', + 'billing', + 'settings', + 'news', +]; +const navigationItemsIcons = [ + Icons.LiveChatMono, + Icons.Messages, + Icons.Automation, + Icons.Archives, + Icons.Tickets, + Icons.People, + Icons.Report, + Icons.Apps, +]; +const secondaryNavigationIcons = [ + Icons.CreditCardOutline, + Icons.Settings, + Icons.Notifications, +]; + +const defaultImage = + 'https://cdn-labs.livechat-files.com/api/file/lc/img/100019504/df59da4b5b0cdb6030efb08787fd255d.jpg'; + +export const Example = (): ReactElement => { + const [activeItem, setActiveItem] = useState('archives'); + const [activeSubItem, setActiveSubItem] = useState(0); + const [topBarVisible] = useState(true); + const [visibleAlert, setVisibleAlert] = useState(0); + + const { products } = useProductSwitcher({ + env: 'labs', + installedProducts: [ + { + product: 'ChatBot', + }, + { + product: 'HelpDesk', + }, + { + product: 'KnowledgeBase', + }, + { + product: 'LiveChat', + }, + { + product: 'OpenWidget', + }, + ], + organizationId: 'organizationId', + subscriptions: { + livechat: { status: 'active' }, + chatbot: { status: 'expired' }, + }, + mainProductId: 'livechat', + }); + + const getSubNav = () => { + switch (activeItem) { + case 'chats': + return getChatsMenu(activeSubItem, setActiveSubItem); + case 'engage': + return getEngageSubMenu(activeSubItem, setActiveSubItem); + case 'archives': + return getArchivesSubMenu(activeSubItem, setActiveSubItem); + default: + return null; + } + }; + + const reducer = ( + state: { + isVisible: boolean; + reference: string; + cursorPosition?: string; + cursorTiming?: string; + elementStyles?: CSSProperties; + }, + action: { type: string } + ) => { + if (action.type === 'first-step') { + return { + ...state, + reference: 'first-step', + isVisible: true, + cursorPosition: 'right-end', + }; + } + if (action.type === 'last-step') { + return { + ...state, + reference: 'last-step', + }; + } + if (action.type === 'home') { + return { + ...state, + reference: 'home', + cursorPosition: 'right-start', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'archives') { + return { + ...state, + reference: 'archives', + cursorPosition: 'right-start', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'user') { + return { + ...state, + reference: 'user', + cursorPosition: 'right-end', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'chat-list-column') { + return { + ...state, + reference: 'chat-list-column', + cursorPosition: 'right', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'text-area') { + return { + ...state, + reference: 'text-area', + cursorPosition: 'top', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'action-bar-area') { + return { + ...state, + reference: 'action-bar-area', + cursorPosition: 'left', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'one') { + return { + ...state, + reference: 'one', + cursorPosition: 'bottom-end', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'action-bar-area-menu-button') { + return { + ...state, + reference: 'action-bar-area-menu-button', + cursorPosition: 'bottom-end', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'accordion') { + return { + ...state, + reference: 'accordion', + cursorPosition: 'left', + cursorTiming: 'moderate2', + }; + } + if (action.type === 'isVisible') { + return { + reference: 'first-step', + isVisible: !state.isVisible, + cursorPosition: 'right-end', + }; + } + + return state; + }; + + const [state, dispatch] = useReducer(reducer, { + reference: 'first-step', + isVisible: false, + }); + + const [isCompleted, setisCompleted] = useState(false); + + return ( + <> + + +
  • + +
  • + {navigationItems.slice(0, 8).map((item, index) => ( + } + onClick={(e, id) => { + e.preventDefault(); + setActiveItem(id); + }} + isActive={activeItem === item} + badge={getBadgeContent(item)} + /> + ))} +
    + + { + e.preventDefault(); + setActiveItem(id); + }} + /> + {navigationItems.slice(8, 11).map((item, index) => ( + } + onClick={(e, id) => { + e.preventDefault(); + setActiveItem(id); + }} + isActive={activeItem === item} + /> + ))} + + } + > + Custom element with own tooltip (native nav tooltip is + disabled) + + } + onClick={(e) => e.preventDefault()} + /> + + + } + mobileNavigation={ + + {navigationItems.slice(0, 5).map((item, index) => ( + } + onClick={(e, id) => { + e.preventDefault(); + setActiveItem(id); + }} + isActive={activeItem === item} + badge={getBadgeContent(item)} + /> + ))} + + } + sideNavigation={getSubNav()} + topBar={ + topBarVisible ? ( + + ) : null + } + > + dispatch({ type: 'first-step' })} + /> +
    + {state.isVisible && ( + + {state.reference === 'first-step' ? ( + } + message="We have prepared the playground ready for you to test all capabilities of chat section. If you have any question, just trigger me from the upper -right corner of the screen!" + cta={ + <> + + + + } + /> + ) : null} + + {state.reference === 'last-step' ? ( + setisCompleted(true)} + headerMessage="Thanks for joining my tour" + headerIcon={} + message="We have prepared the playground ready for you to test all capabilities of chat section. If you have any question, just trigger me from the upper -right corner of the screen!" + cta={ + <> + + + + } + /> + ) : null} + + {state.reference === 'home' ? ( + dispatch({ type: 'archives' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'archives' ? ( + dispatch({ type: 'user' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'user' ? ( + dispatch({ type: 'chat-list-column' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'chat-list-column' ? ( + dispatch({ type: 'text-area' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'text-area' ? ( + dispatch({ type: 'action-bar-area' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'action-bar-area' ? ( + dispatch({ type: 'one' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'one' ? ( + + dispatch({ type: 'action-bar-area-menu-button' }) + } + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'action-bar-area-menu-button' ? ( + dispatch({ type: 'accordion' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + {state.reference === 'accordion' ? ( + dispatch({ type: 'last-step' })} + handleCloseAction={() => dispatch({ type: 'isVisible' })} + /> + ) : null} + + )} + + ); +}; + +export const UserGuideStepExample = (): ReactElement => { + return ( + {}} + handleCloseAction={() => {}} + /> + ); +}; +UserGuideStepExample.parameters = { + layout: 'centered', +}; + +export const UserGuideBubbleStepExample = (): ReactElement => { + return ( + } + message="We have prepared the playground ready for you to test all capabilities of chat section. If you have any question, just trigger me from the upper -right corner of the screen!" + cta={ + <> + + + + } + /> + ); +}; +UserGuideBubbleStepExample.parameters = { + layout: 'centered', +}; diff --git a/packages/react-components/src/components/UserGuide/UserGuide.tsx b/packages/react-components/src/components/UserGuide/UserGuide.tsx new file mode 100644 index 000000000..aa411d434 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/UserGuide.tsx @@ -0,0 +1,191 @@ +import { FC, PropsWithChildren, useEffect, useRef, useState } from 'react'; + +import { FloatingPortal } from '@floating-ui/react'; +import { autoUpdate, flip, shift, useFloating } from '@floating-ui/react-dom'; +import cx from 'clsx'; + +import cursorImage from '../../stories/assets/cursor.svg'; + +import { IUserGuide } from './types'; +import VirtualReference from './virtualElementReference'; + +import styles from './UserGuide.module.scss'; + +const baseClass = 'user-guide'; + +const virtualReference = (element: Element, padding: number = 0) => + new VirtualReference(element, padding); + +export const UserGuide: FC> = ({ + className, + children, + cursorPosition = 'bottom-start', + cursorTiming = 'fast2', + parentElementName, + isVisible = false, + elementStyles, + zIndex, + isFirstStep, + isLastStep, +}) => { + const [parentElement, setParentElement] = useState(null); + const [rect, setRect] = useState(null); + const containerRef = useRef(null); + const contentRef = useRef(null); + const cursorPlacement = + isFirstStep || isLastStep ? 'bottom-start' : cursorPosition; + + const { refs, x, y, strategy, placement, update } = useFloating({ + middleware: [shift(), flip()], + placement: cursorPlacement, + open: true, + whileElementsMounted: autoUpdate, + }); + + const handleViewportChange = () => { + if (parentElement) { + setRect( + virtualReference(parentElement).getBoundingClientRect() as DOMRect + ); + } + }; + + useEffect(() => { + if (parentElement !== null) { + window.addEventListener('resize', handleViewportChange); + window.addEventListener('scroll', handleViewportChange); + + return () => { + window.removeEventListener('resize', handleViewportChange); + window.removeEventListener('scroll', handleViewportChange); + }; + } + }, [parentElement]); + + useEffect(() => { + window.addEventListener('resize', update); + + return () => window.removeEventListener('resize', update); + }, [update]); + + useEffect(() => { + if (parentElementName) { + const element = document.querySelector(parentElementName); + setParentElement(element); + refs.setReference(element); + } + }, [parentElementName]); + + useEffect(() => { + parentElement && + setRect( + virtualReference(parentElement).getBoundingClientRect() as DOMRect + ); + }, [parentElement]); + + useEffect(() => { + if (parentElement && containerRef.current) { + containerRef.current.innerHTML = ''; + const clonedElement = parentElement.cloneNode(true) as HTMLElement; + Object.assign(clonedElement.style, elementStyles); + containerRef.current.appendChild(clonedElement); + } + + return () => { + if (containerRef.current) { + containerRef.current.innerHTML = ''; + } + }; + }, [parentElement, containerRef.current]); + + useEffect(() => { + if (isVisible) { + document.body.classList.add('user-guide-visible'); + } else { + document.body.classList.remove('user-guide-visible'); + } + + return () => { + document.body.classList.remove('user-guide-visible'); + }; + }, [isVisible]); + + const cloneReferenceElement = () => { + if (!rect) return null; + + return ( +
    + ); + }; + + return ( + <> +
    + {parentElement && cloneReferenceElement()} +
    + {isVisible && ( + +
    +
    +
    + cursor +
    + {children && ( +
    + {children} +
    + )} +
    +
    +
    + )} + + ); +}; diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.module.scss b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.module.scss new file mode 100644 index 000000000..92c4d87b9 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.module.scss @@ -0,0 +1,118 @@ +$base-class: 'user-guide-bubble-step'; + +.#{$base-class} { + display: flex; + position: relative; + flex-direction: column; + gap: 1px; + width: 340px; + color: var(--content-basic-primary); + + &__bubble-wrapper { + position: relative; + width: fit-content; + + &::before { + position: absolute; + inset: 0; + z-index: -2; + border-radius: var(--radius-4); + box-shadow: var(--shadow-modal); + content: ''; + } + + &--completed { + &::before { + animation-name: shadow-glow; + animation-duration: var(--transition-duration-slow-2); + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + } + } + } + + &__bubble { + position: relative; + transition: border-radius var(--transition-duration-fast-1) ease-in-out; + opacity: 0; + z-index: 1; + border-radius: var(--radius-4); + background: var(--surface-ai-copilot-basic-default); + padding: var(--radius-4); + width: fit-content; + min-height: 53px; + overflow: hidden; + animation-name: fade-in; + animation-duration: var(--transition-duration-moderate-2); + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + + &--next-msg { + border-bottom-left-radius: 0; + } + + &--header { + display: flex; + flex-direction: row; + gap: var(--spacing-2); + align-items: center; + } + + &--message { + border-top-left-radius: 0; + } + + &--cta { + display: flex; + gap: var(--spacing-2); + justify-content: space-between; + border-top-left-radius: 0; + } + } +} + +@keyframes shadow-glow { + 0% { + box-shadow: var(--shadow-modal); + } + + 50% { + box-shadow: var(--shadow-tour-animation); + } + + 100% { + box-shadow: var(--shadow-modal); + } +} + +@keyframes background-glow { + 0% { + opacity: 0; + } + + 15% { + opacity: 0; + } + + 50% { + opacity: 1; + } + + 85% { + opacity: 0; + } + + 100% { + opacity: 0; + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.spec.tsx b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.spec.tsx new file mode 100644 index 000000000..2f703b68e --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.spec.tsx @@ -0,0 +1,45 @@ +import { render, userEvent, vi } from 'test-utils'; + +import { IUserGuideBubbleStepProps } from './types'; +import { UserGuideBubbleStep } from './UserGuideBubbleStep'; + +const DEFAULT_PROPS: IUserGuideBubbleStepProps = { + headerMessage: 'Header', + message: 'Text', + cta: , + disableTypingAnimations: true, +}; + +const renderComponent = (props: IUserGuideBubbleStepProps) => + render(); + +describe(' component', () => { + it('should render all default elements', () => { + const { getByText, getByRole } = renderComponent(DEFAULT_PROPS); + + expect(getByText('Header')).toBeInTheDocument(); + expect(getByText('Text')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Primary' })).toBeInTheDocument(); + }); + + it('should render icon if provided', () => { + const { getByTestId } = renderComponent({ + ...DEFAULT_PROPS, + headerIcon:
    , + }); + + expect(getByTestId('icon')).toBeInTheDocument(); + }); + + it('should call handleAnimationComplete when all bubbles are visible', () => { + const handleAnimationComplete = vi.fn(); + const { getByRole } = renderComponent({ + ...DEFAULT_PROPS, + handleAnimationComplete, + }); + + userEvent.click(getByRole('button', { name: 'Primary' })); + + expect(handleAnimationComplete).toHaveBeenCalled(); + }); +}); diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.tsx b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.tsx new file mode 100644 index 000000000..010ef0feb --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/UserGuideBubbleStep.tsx @@ -0,0 +1,114 @@ +import { FC, useState, useEffect } from 'react'; + +import cx from 'clsx'; + +import { AnimatedTextContainer } from '../../../AnimatedTextContainer'; +import { Text } from '../../../Typography'; + +import { IUserGuideBubbleStepProps } from './types'; + +import styles from './UserGuideBubbleStep.module.scss'; + +const baseClass = 'user-guide-bubble-step'; + +export const UserGuideBubbleStep: FC = ({ + headerMessage, + headerIcon, + message, + cta, + isCompleted, + handleAnimationComplete, + disableTypingAnimations, +}) => { + const [visibleBubbles, setVisibleBubbles] = useState(['header']); + const isHeaderVisible = visibleBubbles.includes('header'); + const isMessageVisible = visibleBubbles.includes('message'); + const isCtaVisisble = cta && visibleBubbles.includes('cta'); + + useEffect(() => { + if (visibleBubbles.length === 3) { + handleAnimationComplete && handleAnimationComplete(); + } + }, [visibleBubbles]); + + return ( +
    + {isHeaderVisible && ( +
    +
    + {headerIcon && ( +
    + {headerIcon} +
    + )} + + + setVisibleBubbles([...visibleBubbles, 'message']) + } + /> + +
    +
    + )} + {isMessageVisible && ( +
    +
    + + + setVisibleBubbles([...visibleBubbles, 'cta']) + } + /> + +
    +
    + )} + {isCtaVisisble && ( +
    +
    + {cta} +
    +
    + )} +
    + ); +}; diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/types.ts b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/types.ts new file mode 100644 index 000000000..75da3000f --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideBubbleStep/types.ts @@ -0,0 +1,32 @@ +import { ReactElement, ReactNode } from 'react'; + +export interface IUserGuideBubbleStepProps { + /** + * The message for the first bubble + */ + headerMessage: string; + /** + * The optional icon for the first bubble + */ + headerIcon?: ReactElement; + /** + * The message for the second bubble + */ + message: string; + /** + * The cta for the third bubble + */ + cta: ReactNode; + /** + * Set to true to show the completed state + */ + isCompleted?: boolean; + /** + * The function to be called when the all bubbles animations complete + */ + handleAnimationComplete?: () => void; + /** + * Set to true to disable typing animations + */ + disableTypingAnimations?: boolean; +} diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.module.scss b/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.module.scss new file mode 100644 index 000000000..9cfcb4049 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.module.scss @@ -0,0 +1,62 @@ +$base-class: 'user-guide-step'; + +.#{$base-class} { + display: flex; + flex-direction: column; + border-radius: var(--radius-4); + box-shadow: var(--shadow-modal); + background: var(--surface-ai-copilot-basic-default); + padding: var(--spacing-4); + width: 340px; + + &__heading { + margin-bottom: var(--spacing-3); + color: var(--content-basic-primary); + } + + &__content { + margin-bottom: var(--spacing-4); + color: var(--content-basic-secondary); + } + + &__image-wrapper { + display: flex; + justify-content: center; + margin-bottom: var(--spacing-4); + } + + &__footer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + animation-name: fade-in; + animation-duration: var(--transition-duration-moderate-2); + animation-timing-function: ease-in-out; + animation-fill-mode: forwards; + + &__step-counter { + white-space: nowrap; + color: var(--content-basic-secondary); + } + + &__button-primary { + align-self: flex-end; + } + + &__button-close { + margin-right: var(--spacing-1); + color: var(--content-basic-secondary); + } + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.spec.tsx b/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.spec.tsx new file mode 100644 index 000000000..bfdbe8c14 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.spec.tsx @@ -0,0 +1,77 @@ +import { render, userEvent, vi } from 'test-utils'; + +import { IUserGuideStepProps } from './types'; +import { UserGuideStep } from './UserGuideStep'; + +const DEFAULT_PROPS: IUserGuideStepProps = { + header: 'Header', + text: 'Text', + currentStep: 1, + stepMax: 2, + handleClickPrimary: vi.fn(), +}; + +const renderComponent = (props: IUserGuideStepProps) => + render(); + +describe(' component', () => { + it('should render all default elements', () => { + const { getByText, getByRole } = renderComponent(DEFAULT_PROPS); + + expect(getByText('Header')).toBeInTheDocument(); + expect(getByText('Text')).toBeInTheDocument(); + expect(getByText('Step 1 of 2')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); + }); + + it('should render image if provided', () => { + const { getByAltText } = renderComponent({ + ...DEFAULT_PROPS, + image: { src: 'image.jpg', alt: 'Image' }, + }); + + expect(getByAltText('Image')).toBeInTheDocument(); + }); + + it('should render video if provided and no image', () => { + const { getByTestId } = renderComponent({ + ...DEFAULT_PROPS, + video: { src: 'video.mp4' }, + }); + + expect(getByTestId('user-guide-step-video')).toBeInTheDocument(); + }); + + it('should call handleClickPrimary when primary button is clicked', () => { + const handleClickPrimary = vi.fn(); + const { getByRole } = renderComponent({ + ...DEFAULT_PROPS, + handleClickPrimary, + }); + + userEvent.click(getByRole('button', { name: 'Next' })); + + expect(handleClickPrimary).toHaveBeenCalled(); + }); + + it('should call handleCloseAction when skip button is clicked', () => { + const handleCloseAction = vi.fn(); + const { getByRole } = renderComponent({ + ...DEFAULT_PROPS, + handleCloseAction, + }); + + userEvent.click(getByRole('button', { name: 'Skip all' })); + + expect(handleCloseAction).toHaveBeenCalled(); + }); + + it('should change primary button content when currentStep is equal to stepMax', () => { + const { getByRole } = renderComponent({ + ...DEFAULT_PROPS, + currentStep: 2, + }); + + expect(getByRole('button', { name: 'Finish' })).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.tsx b/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.tsx new file mode 100644 index 000000000..4bb52ea4d --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideStep/UserGuideStep.tsx @@ -0,0 +1,111 @@ +import { FC, useEffect, useState } from 'react'; + +import cx from 'clsx'; + +import { AnimatedTextContainer } from '../../../AnimatedTextContainer'; +import { Button } from '../../../Button'; +import { Text } from '../../../Typography'; + +import { IUserGuideStepProps } from './types'; + +import styles from './UserGuideStep.module.scss'; + +const baseClass = 'user-guide-step'; + +export const UserGuideStep: FC = ({ + header, + text, + image, + video, + currentStep, + stepMax, + typingAnimation = false, + handleCloseAction, + handleClickPrimary, +}) => { + const [isTypingEnd, setIsTypingEnd] = useState(false); + + useEffect(() => { + if (handleCloseAction) { + document.addEventListener('keydown', handleCloseAction); + + return () => { + document.removeEventListener('keydown', handleCloseAction); + }; + } + }, [handleCloseAction]); + + return ( +
    + + {header} + + {image && ( +
    + {image.alt} +
    + )} + {video && !image && ( +
    + ); +}; diff --git a/packages/react-components/src/components/UserGuide/components/UserGuideStep/types.ts b/packages/react-components/src/components/UserGuide/components/UserGuideStep/types.ts new file mode 100644 index 000000000..5072acedb --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/UserGuideStep/types.ts @@ -0,0 +1,50 @@ +import { MouseEvent } from 'react'; + +export interface IUserGuideStepProps { + /** + * The header of the step + */ + header: string; + /** + * The text of the step + */ + text: string; + /** + * Set to enable typing animation for the text + */ + typingAnimation?: boolean; + /** + * The image of the step + */ + image?: { + src: string; + alt: string; + }; + /** + * The video of the step. It will be rendered if there is no image + */ + video?: { + src: string; + playsInline?: boolean; + autoPlay?: boolean; + muted?: boolean; + loop?: boolean; + controls?: boolean; + }; + /** + * The current step number + */ + currentStep: number; + /** + * The maximum number of steps + */ + stepMax: number; + /** + * The function to be called when the primary button is clicked + */ + handleClickPrimary: () => void; + /** + * The function to be called when the the skip button is clicked + */ + handleCloseAction?: (ev: KeyboardEvent | MouseEvent) => void; +} diff --git a/packages/react-components/src/components/UserGuide/components/index.ts b/packages/react-components/src/components/UserGuide/components/index.ts new file mode 100644 index 000000000..16a60b02c --- /dev/null +++ b/packages/react-components/src/components/UserGuide/components/index.ts @@ -0,0 +1,2 @@ +export { UserGuideBubbleStep } from './UserGuideBubbleStep/UserGuideBubbleStep'; +export { UserGuideStep } from './UserGuideStep/UserGuideStep'; diff --git a/packages/react-components/src/components/UserGuide/index.ts b/packages/react-components/src/components/UserGuide/index.ts new file mode 100644 index 000000000..11644c689 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/index.ts @@ -0,0 +1,3 @@ +export { UserGuide } from './UserGuide'; +export { UserGuideStep, UserGuideBubbleStep } from './components/'; +export type { IUserGuide } from './types'; diff --git a/packages/react-components/src/components/UserGuide/placeholder.png b/packages/react-components/src/components/UserGuide/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..1cf58b6f999a44e1a1f2e32d5d1242165a9df466 GIT binary patch literal 5381 zcmZ9QcTiJZx5jBgXc9rGBA|#00YXPW0uf)jR4D6!*?5XeP=7k% z7(e>fICw;RVetEpq6pCw%8ZUMU5%2x(%p^$>E=`GkX;Iz}zv-UOCgMq}x z5{73W519l#Ui;1t`Kn9Rd*^*ESNqDl-FI4B+^AD>M629iuJXu8H|#3pG8 zd(V&~Q_Ddtf7-rnI)R=SVC9=ugtU^fGMOGpy5Hf^{q>gWLhba_aetLqvpMxnr1bV7S@*q_} zVF!j%0C2g#+ip!Dwl>|Z2SPWAv6|q7FHk@kl_G#^d?`848XCbaXO#|YBDven%UN;g zL|F#32GQSuMm!;eMbwK4xfI{jVA=}~0r~{=4R_sd zhu&^T4*;6e#Trr|Z*ql9l3FHSrOOl2RkI1{uLrM^Dq|u&Cg=%m2&)n{sGWAlJ^vPB z$BmM5wcq)-KFwKehym4qDl+Ey8sowq{&yEh(KN4q)zkNpp)tPy))!v;MQ{G-s{Oym z0awREt~yvYw#OtYLhVU}v4?+zx&wfz0ew=0|MtJ%6(YO+AY$m9fs_>foB;!onE|L8O;^Fqm1gopJ3X4>FHs7Z^>KD*L&vJCLJ^H_Cdh z+;?&mv@}!-cRFVZIUz;R4tMqU6|yP}l8Bf3ZOxsb#sA>@=T9&ffy9QDg$kE9{5dP+ zQ6K@IdEcE7vi321CfLkxRXPYaeUu8Gf)-ZWw1@FI48USJ2J>G@Zc^bhAandLBIhXg z*UEkkpJ-KN0GoSKR`wGVQQ!L~Jr<-(&`a~kO`P+zGw%0dYPr|JKX}l|sZmp++wu3I zOpS|t`HPdimwOGGt2sH)M<7tVb__V#?%~dv&vqgOM=j(J?$Ig4?GDUwuKtH_uh~XH z==WR&_iwn^=S@8z18Hs*Mbm^z05pLbEYBnY<@ z!QBT+<=27?kmTazQxcaOhWEiu!_d97vybvNaHX`iCz5nrL8in2IKGISe{ta?@`*Qn z^I`~RgYQYi=Ea79FD*3KXGH783!#1!hZo1JT-!RWxtHHW7kn>61}Ihvzp5DM9jVj` zR=ylgxjw;`Kp9oPgt*iTI<8Ey`N$Z;$x~rM3EQ%k=s`8j^O?X;s7Lx-D(N;Z(=knA z>NZ*&fZ`&bPxV%03?y`V3;4i$P>rb}1sayUs@jOaUU)ilGonAkVFySiaT1&teWL#fc!U(xGw8J#8$x+M0M zbxCnORC0zP({1Js#9n9aDN9M-)_VkY^uZyurzBZZMs8_Q8A)6*v-fG`_IB3iax89K8< znDHlZUmCa?>!3N%)TYTeaF^xH&J1@RZ=;@aI;a6Q0{pXgxaDecWtdAGw*J4t79bFcBEQb2LWI_@+Tl6?JH#Px|z!Jdw~Em%_q%5*{{L zwtEc%4BCslJWCjTJ0HJ)1ju-tHQdtrnPVDL@+Of9b)NHW{Y%LXZPu1)hJ{u)$e@L( zOT^%JtH|IQQ0Zjum8MlZtslRvh57VB)toRxxAe<7<+pNHq~QyW5#w)-?^(JFt>)@i zG>3$(o`0rq9hxM$$>?9&ZIRL9g>>(Dg0C@!oh7vS@IP)+!^8=}B-k`PPO38XzfI(C z8$@-P-})JK7G`x;TSM(H&oXvdc0y(XOuakqCCZbrCrfd#UkF>A#-9iv`kixo9rF)< z{@#Ddw{cxR8(jS^yk2K!{&uY4_KS<;7T#h*p)ifdnT)opE_2qY%|mhOL$r4K0Iv4y z18EMotnFp){*^XzZ}PA&*+FoEq6RV=6NGUxoYV(F(AmF2eI=BCm9C@ZPtj@Lq>WtI z8HuyBjiXa8=EE5PBcb}qw%PE2~&8@^{? z>*$ktz)F}&Bl$XZV_D1^4re2?)NP$OkWuEct@~=aL5a8Hwbton{mjS?2ZcNvDGgXI zrZM4xhAh(%J9v^UR=+7zF!Y&L(JL{rwu)%f!fcJ#ObC1yr04h*ZEF7J%jvB+=T*GS92b}^dy72bk?X0kTw?Kuo@k`T!$8XJ zrHPmQvz|F}8Tf%(6jM-x*G#UgzqT8X)Xku(jrm6jF?^G56v5ghqLz`*r8%o+=qGk* zM33c=-C_JX<@on~aV;x}0V+Piez=WeYRj=s<@6m3w3^aVP~q(}EzQ)u!X^F8sP5L` z!hr`e^syP=-Zl=egks$wv>Wn|#d)Rr_w%+DLY+5UM^ArjNArvjDdjniLyGwh@7}gy@|PLPkC~62)j!->-O|~s7SuzVsp%ri9Z6_g zUsbDPa}Ijq`qXIh&pc*{gIeZt$VW1KZ}5`=>*L(KykkdYf4hHY0oOA569u!fH(+*V zG2mqI$>6C}{|NEaUKQn&(pTQuaKfbn)#${v1! z-)FutmtV`l!%a3lGY^@`6x7&u%Y5pCDuWb`3~lB2T|lPTl-C(F{kffJ*Rw$RCf6I& zI2`##$bmSE@E~4@1itm!T?iZ{SJjaV()DV}0e$O#Gc4*5Y*A_S%uQFFbGto?5-~rV@~=uCWPPG#tL7HvI-@(y7Pwyq*U|%cIP^6Nk%S8)^5?F2?CgSG7)l@i1HNWULveHNZeMx9YhW&M361Vd4bSJnW-N$MG_Mo z2!mf)gs*hLB0586xjpHiin0)wJ$9#fh20?e{SumG@%)syfAghNnB%JY2%Y|quPrDS zXsYuRjSUS-jghF&&nEroSYxB<^&Opo^IPO(mFBi@PQmY%sO0WN`D}bRYf+TQV$TZ9 z8frN8!aEY#5up(cpLWVq!GbSo=@z_sGxWW#^I>7X6z|U~mYAduHk8I5PKR0V-C(_w zGWR>YX}Oj>sdmSaA$DpJk*|k^8@p1eeYu?7DdBFr8M08}r)u8Z!i3A)&mJsGepB;g z`?)4_LwG#h%G*8!A)yk#;1OIpv$=R`~LfwIF$fJnZX z$SA=|dvZ@#A*qGNb#yE@u!Ge&cyOuyu?gH-_dG@NRlca<99I7I=YI-pJu2Du+b66` zVVd(@8SmF{ntuEWgDSvR8dF)>cXU^UaLNG5{Z5xU^L@xJ6qS)ys$`8@wf9mxJ$t7N zedbjk(>M+o4ugFBBiu|CbQCDgC*ZOmTYgZa^qoJQg^3<>H`WkKy1s-o4IQ?F%HPY< zarb+YgA@d*iqnx!yS_@cYG!8OodTs6-S%5QO%F$gz0ja(MGPN~_rn)HQg$M7^9{md zTrctXY2j3i^K0>8MA3JV616r8}6I#7d@KUX@vVptf95+rTd^}=IPE`gn~l<7TJ$1fv(oYPQ`{5 zx!P>DugN7eQo34C&eud3q{5hgbgoKGdQUp@W}B6L!>;?nA1@W>_;WD**)M>I`TO5v zf(94ECtERd-bn&sBMP(0fJ6luThxW$Emoe#wLr={`FP5uVBo-hUXd-r(`a+OLtMFiJSVy^ zscx0Z>#jH>)Y{xfqyv99VutR}UO+e&*Oxso-n{$ODn>aEDwA@dJxrGUT$N(m~unE2e#~6{;R<5QTmH zap!mnvwlE5r+S4Ly~De@4#+kWPs#i9j`mO{fad{piXg<6HAb{{RP#7}$FxyFUU1fC ztySxhnwpTTMNnHxbHBYho*TU5Ypl>_8Wx^^h&nn5a31nRJrA(7V{I+S;ZIx{`E%u+?P5Mhkj}l`k`UHAja@{Nu)%+glpW6oyv0& z-S{z!dRp+OQI4M@j2&>YD-TN71Mi@b6jmD-n7QL; zd67%)0gp^J);btzOaUQ>_^oL9(Wn{Bs04q^u-vRL~4};JqzExhf-q zpEGGtFNJ7)^d5Vg`4;+x|C_!B`f`ZH9DW2{KBjl-F(68fAr}t$eneo8s`yrFT;tA`7rkSqxZeH05^1t9DUzh++2xdeojo^0v zO*i!HDj*L5n$r>pm6EBAC~jcV;CD8OUy^Efnl<gko+oSs&H^Qd{X5&yME`Y5V2PrBZxfZYVk z@%aN9H2fYu=qZc+wZwMs?7U!!?TfRut void }): ReactElement => { + return ( +
    +
    +
    + Chats +
    +
    + {[...Array(7)].map((_, index) => ( +
    + + + + + + + +
    + ))} +
    +
    +
    +
    + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + + +
    +
    +
    +
    + {})} + /> +
    +
    + + Section 1 content + + + Section 2 content + + + Section 3 content + +
    +
    +
    + ) +} \ No newline at end of file diff --git a/packages/react-components/src/components/UserGuide/styles/transitions.scss b/packages/react-components/src/components/UserGuide/styles/transitions.scss new file mode 100644 index 000000000..8344cb1d0 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/styles/transitions.scss @@ -0,0 +1,17 @@ +@mixin durations() { + &--fast1 { + transition-duration: var(--transition-duration-fast-1); + } + + &--fast2 { + transition-duration: var(--transition-duration-fast-2); + } + + &--moderate1 { + transition-duration: var(--transition-duration-moderate-1); + } + + &--moderate2 { + transition-duration: var(--transition-duration-moderate-2); + } +} diff --git a/packages/react-components/src/components/UserGuide/types.ts b/packages/react-components/src/components/UserGuide/types.ts new file mode 100644 index 000000000..1ecfa4395 --- /dev/null +++ b/packages/react-components/src/components/UserGuide/types.ts @@ -0,0 +1,44 @@ +import { CSSProperties } from 'react'; + +import { Placement } from '@floating-ui/react'; + +export type CursorTiming = 'fast1' | 'fast2' | 'moderate1' | 'moderate2'; + +export interface IUserGuide { + /** + * The class name for the floating container + */ + className?: string; + /** + * The CSS properties for the highlighted element + */ + elementStyles?: CSSProperties; + /** + * The position for the floating element which sets the cursor position + */ + cursorPosition?: Placement; + /** + * The timing for the floating element transition + */ + cursorTiming?: CursorTiming; + /** + * The id of the element to highlight + */ + parentElementName?: string; + /** + * The visibility of the user guide + */ + isVisible?: boolean; + /** + * The custom z-index value for the overlay + */ + zIndex?: number; + /** + * The first step of the user guide, rendered on the center of the screen + */ + isFirstStep?: boolean; + /** + * The last step of the user guide, rendered on the center of the screen + */ + isLastStep?: boolean; +} diff --git a/packages/react-components/src/components/Tooltip/components/UserGuide/virtualElementReference.ts b/packages/react-components/src/components/UserGuide/virtualElementReference.ts similarity index 77% rename from packages/react-components/src/components/Tooltip/components/UserGuide/virtualElementReference.ts rename to packages/react-components/src/components/UserGuide/virtualElementReference.ts index 09f33b544..9ecf66c1d 100644 --- a/packages/react-components/src/components/Tooltip/components/UserGuide/virtualElementReference.ts +++ b/packages/react-components/src/components/UserGuide/virtualElementReference.ts @@ -8,10 +8,10 @@ export default class VirtualReference { } addPadding(rect: DOMRect): Omit { - const x = Math.round(rect.left) - this.padding; - const y = Math.round(rect.top) - this.padding; - const width = Math.round(rect.width) + 2 * this.padding; - const height = Math.round(rect.height) + 2 * this.padding; + const x = Math.ceil(rect.left) - this.padding; + const y = Math.ceil(rect.top) - this.padding; + const width = Math.ceil(rect.width) + 2 * this.padding; + const height = Math.ceil(rect.height) + 2 * this.padding; const top = y; const left = x; const bottom = top + height; diff --git a/packages/react-components/src/foundations/shadow.css b/packages/react-components/src/foundations/shadow.css index f90e81662..babfc9e17 100644 --- a/packages/react-components/src/foundations/shadow.css +++ b/packages/react-components/src/foundations/shadow.css @@ -25,6 +25,12 @@ --shadow-fixed-bottom: 0 2px 10px rgb(19 19 23 / 18%); --focus-ring-inner: inset 0 0 1px 2px var(--action-primary-default); --state-active-field: 0 0 0 4px rgb(0 89 225 / 15%); + --shadow-tour-animation: 0 8px 32px 1px rgb(150 0 209 / 80%), + 0 -8px 32px 0 rgba(5 172 255 / 90%), 0 8px 16px 0 rgb(5 172 255 / 20%) inset, + 0 -8px 16px 1px rgba(150 0 209 / 10%) inset, + 0 8px 32px 1px rgb(150 0 209 / 80%), 0 -8px 32px 0 rgb(5 172 255 / 90%), + 0 8px 16px 0 rgb(5 172 255 / 20%) inset, + 0 -8px 16px 1px rgb(150 0 209 / 10%) inset; --shadow-ai-copilot-animation-start: 0 -4px 22px 1px rgb(0 102 255 / 30%), 0 12px 32px 1px rgb(113 111 255 / 60%), 0 0 0 1px rgb(145 70 255 / 100%) inset; @@ -76,6 +82,12 @@ --shadow-fixed-bottom: 0 2px 10px rgb(19 19 23 / 18%); --focus-ring-inner: inset 0 0 1px 2px var(--action-primary-default); --state-active-field: 0 0 0 4px rgb(104 175 255 / 25%); + --shadow-tour-animation: 0 8px 32px 1px rgb(150 0 209 / 80%), + 0 -8px 32px 0 rgba(5 172 255 / 90%), 0 8px 16px 0 rgb(5 172 255 / 20%) inset, + 0 -8px 16px 1px rgba(150 0 209 / 10%) inset, + 0 8px 32px 1px rgb(150 0 209 / 80%), 0 -8px 32px 0 rgb(5 172 255 / 90%), + 0 8px 16px 0 rgb(5 172 255 / 20%) inset, + 0 -8px 16px 1px rgb(150 0 209 / 10%) inset; --shadow-ai-copilot-animation-start: 0 0 3px 0 hsl(261deg 100% 78% / 100%) inset, 0 -4px 30px 1px hsl(216deg 100% 50% / 30%), diff --git a/packages/react-components/src/foundations/transition.css b/packages/react-components/src/foundations/transition.css index 9cb88da3e..072fad859 100644 --- a/packages/react-components/src/foundations/transition.css +++ b/packages/react-components/src/foundations/transition.css @@ -3,6 +3,8 @@ --transition-duration-fast-1: 100ms; --transition-duration-fast-2: 200ms; --transition-duration-moderate-1: 300ms; + --transition-duration-moderate-2: 500ms; + --transition-duration-slow-2: 1500ms; /* Timing */ --transition-timing-ease-in: ease-in; @@ -23,6 +25,8 @@ --transition-duration-fast-1: 0ms; --transition-duration-fast-2: 0ms; --transition-duration-moderate-1: 0ms; + --transition-duration-moderate-2: 0ms; + --transition-duration-slow-2: 0ms; --delay-instant: 0ms; --delay-moderate: 0ms; --delay-slow: 0ms; @@ -35,6 +39,8 @@ --transition-duration-fast-1: 0ms; --transition-duration-fast-2: 0ms; --transition-duration-moderate-1: 0ms; + --transition-duration-moderate-2: 0ms; + --transition-duration-slow-2: 0ms; --delay-instant: 0ms; --delay-moderate: 0ms; --delay-slow: 0ms; diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index 945b254cb..eabd2074c 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -8,6 +8,7 @@ export * from './components/Accordion'; export * from './components/ActionBar'; export * from './components/ActionCard'; export * from './components/ActionMenu'; +export * from './components/AnimatedTextContainer'; export * from './components/Alert'; export * from './components/AppFrame'; export * from './components/AutoComplete'; diff --git a/packages/react-components/src/stories/assets/cursor.svg b/packages/react-components/src/stories/assets/cursor.svg new file mode 100644 index 000000000..68241f08e --- /dev/null +++ b/packages/react-components/src/stories/assets/cursor.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/react-components/src/utils/types.ts b/packages/react-components/src/utils/types.ts index 8755f34be..bea9f90b3 100644 --- a/packages/react-components/src/utils/types.ts +++ b/packages/react-components/src/utils/types.ts @@ -3,6 +3,10 @@ import * as React from 'react'; export type Size = 'compact' | 'medium' | 'large'; export interface ComponentCoreProps { + /** + * The ID of the component + */ + id?: string; /** * The children of the component */ From 801de035996692c68909c73993579bd030d3eaab Mon Sep 17 00:00:00 2001 From: Marcin Sawicki Date: Fri, 31 Jan 2025 14:32:58 +0100 Subject: [PATCH 02/11] feat(UserGuide): pre release changes --- .../UserGuide/UserGuide.module.scss | 8 ++++---- .../components/UserGuide/stories-helpers.css | 3 ++- .../components/UserGuide/stories-helpers.tsx | 20 ++++++++++++------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/react-components/src/components/UserGuide/UserGuide.module.scss b/packages/react-components/src/components/UserGuide/UserGuide.module.scss index 338e15d70..fe60ede64 100644 --- a/packages/react-components/src/components/UserGuide/UserGuide.module.scss +++ b/packages/react-components/src/components/UserGuide/UserGuide.module.scss @@ -18,7 +18,7 @@ $animations: ( 'top-end': fade-in-top-end, 'bottom-start': fade-in-bottom-start, 'bottom': fade-in-bottom, - 'bottom-end': fade-in-bottom-end + 'bottom-end': fade-in-bottom-end, ); .#{$base-class} { @@ -74,7 +74,7 @@ $animations: ( &--right-end, &--left-end { - align-items: flex-end + align-items: flex-end; } &--bottom-start, @@ -89,7 +89,7 @@ $animations: ( &--bottom-end, &--top-end { - justify-content: flex-end + justify-content: flex-end; } &--top-start, @@ -126,7 +126,7 @@ $animations: ( &--right-end { top: calc(100% - $arrow-size - 9px); - transform: rotate(-130deg) + transform: rotate(-130deg); } &--bottom-start, diff --git a/packages/react-components/src/components/UserGuide/stories-helpers.css b/packages/react-components/src/components/UserGuide/stories-helpers.css index c3c5c1996..1c5f89955 100644 --- a/packages/react-components/src/components/UserGuide/stories-helpers.css +++ b/packages/react-components/src/components/UserGuide/stories-helpers.css @@ -19,7 +19,8 @@ padding: 8px; } -.left, .right { +.left, +.right { flex-basis: 25%; flex-shrink: 0; max-width: 280px; diff --git a/packages/react-components/src/components/UserGuide/stories-helpers.tsx b/packages/react-components/src/components/UserGuide/stories-helpers.tsx index 9e438c62d..b856db9fa 100644 --- a/packages/react-components/src/components/UserGuide/stories-helpers.tsx +++ b/packages/react-components/src/components/UserGuide/stories-helpers.tsx @@ -9,7 +9,11 @@ import { Heading, Text } from '../Typography'; import './stories-helpers.css'; -export const AppContent = ({ onStartGuideClick }: { onStartGuideClick: () => void }): ReactElement => { +export const AppContent = ({ + onStartGuideClick, +}: { + onStartGuideClick: () => void; +}): ReactElement => { return (
    @@ -18,7 +22,11 @@ export const AppContent = ({ onStartGuideClick }: { onStartGuideClick: () => voi
    {[...Array(7)].map((_, index) => ( -
    +
    @@ -49,9 +57,7 @@ export const AppContent = ({ onStartGuideClick }: { onStartGuideClick: () => voi
    - +
    @@ -80,5 +86,5 @@ export const AppContent = ({ onStartGuideClick }: { onStartGuideClick: () => voi
    - ) -} \ No newline at end of file + ); +}; From 0f37a6f016252d469a0b0b86f62f20c9eb64e7d0 Mon Sep 17 00:00:00 2001 From: Marcin Sawicki Date: Fri, 31 Jan 2025 14:34:34 +0100 Subject: [PATCH 03/11] v2.19.0 --- lerna.json | 2 +- package-lock.json | 12 ++++++------ packages/example-react/package.json | 6 +++--- packages/icons/package.json | 2 +- packages/react-components/package.json | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lerna.json b/lerna.json index 65ab5a292..029d24c39 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "packages": ["packages/*"], - "version": "2.18.0", + "version": "2.19.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package-lock.json b/package-lock.json index ffae8ac86..45892977b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25990,10 +25990,10 @@ } }, "packages/example-react": { - "version": "2.18.0", + "version": "2.19.0", "dependencies": { - "@livechat/design-system-icons": "^2.15.0", - "@livechat/design-system-react-components": "^2.18.0", + "@livechat/design-system-icons": "^2.19.0", + "@livechat/design-system-react-components": "^2.19.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, @@ -26007,7 +26007,7 @@ }, "packages/icons": { "name": "@livechat/design-system-icons", - "version": "2.15.0", + "version": "2.19.0", "devDependencies": { "@svgr/cli": "^8.1.0", "glob": "^10.3.10", @@ -26029,12 +26029,12 @@ }, "packages/react-components": { "name": "@livechat/design-system-react-components", - "version": "2.18.0", + "version": "2.19.0", "license": "ISC", "dependencies": { "@floating-ui/react": "^0.26.25", "@livechat/data-utils": "^0.2.16", - "@livechat/design-system-icons": "^2.15.0", + "@livechat/design-system-icons": "^2.19.0", "clsx": "^1.1.1", "date-fns": "^2.28.0", "lodash.debounce": "^4.0.8", diff --git a/packages/example-react/package.json b/packages/example-react/package.json index e8df564c4..31ed98cc1 100644 --- a/packages/example-react/package.json +++ b/packages/example-react/package.json @@ -1,15 +1,15 @@ { "name": "example-react", "private": true, - "version": "2.18.0", + "version": "2.19.0", "scripts": { "start": "vite --open", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { - "@livechat/design-system-icons": "^2.15.0", - "@livechat/design-system-react-components": "^2.18.0", + "@livechat/design-system-icons": "^2.19.0", + "@livechat/design-system-react-components": "^2.19.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, diff --git a/packages/icons/package.json b/packages/icons/package.json index eea6aaca9..37c3bfc6c 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@livechat/design-system-icons", - "version": "2.15.0", + "version": "2.19.0", "description": "", "publishConfig": { "access": "public" diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 460f4e9d6..f44cb506e 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@livechat/design-system-react-components", - "version": "2.18.0", + "version": "2.19.0", "description": "", "publishConfig": { "access": "public" @@ -76,7 +76,7 @@ "dependencies": { "@floating-ui/react": "^0.26.25", "@livechat/data-utils": "^0.2.16", - "@livechat/design-system-icons": "^2.15.0", + "@livechat/design-system-icons": "^2.19.0", "clsx": "^1.1.1", "date-fns": "^2.28.0", "lodash.debounce": "^4.0.8", From b27548ce4e6c1d8b5812cc91ceb38022e7392bb3 Mon Sep 17 00:00:00 2001 From: Marcin Sawicki Date: Fri, 31 Jan 2025 14:42:37 +0100 Subject: [PATCH 04/11] fix(UserGuide): add missing index export --- packages/react-components/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-components/src/index.ts b/packages/react-components/src/index.ts index eabd2074c..b16957c1a 100644 --- a/packages/react-components/src/index.ts +++ b/packages/react-components/src/index.ts @@ -60,6 +60,7 @@ export * from './components/Tooltip'; export * from './components/Typography'; export * from './components/Textarea'; export * from './components/UpdateBadge'; +export * from './components/UserGuide'; export * from './components/FileUploadProgress'; export * from './components/UploadBar'; export * from './components/FloatingPortal'; From 665e57cd6c938053a2016fcba0d20a0da9128677 Mon Sep 17 00:00:00 2001 From: Marcin Sawicki Date: Fri, 31 Jan 2025 14:45:48 +0100 Subject: [PATCH 05/11] v2.19.1 --- lerna.json | 2 +- package-lock.json | 6 +++--- packages/example-react/package.json | 4 ++-- packages/react-components/package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lerna.json b/lerna.json index 029d24c39..a4cbe1590 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "packages": ["packages/*"], - "version": "2.19.0", + "version": "2.19.1", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package-lock.json b/package-lock.json index 45892977b..6e2421174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25990,10 +25990,10 @@ } }, "packages/example-react": { - "version": "2.19.0", + "version": "2.19.1", "dependencies": { "@livechat/design-system-icons": "^2.19.0", - "@livechat/design-system-react-components": "^2.19.0", + "@livechat/design-system-react-components": "^2.19.1", "react": "^18.3.0", "react-dom": "^18.3.0" }, @@ -26029,7 +26029,7 @@ }, "packages/react-components": { "name": "@livechat/design-system-react-components", - "version": "2.19.0", + "version": "2.19.1", "license": "ISC", "dependencies": { "@floating-ui/react": "^0.26.25", diff --git a/packages/example-react/package.json b/packages/example-react/package.json index 31ed98cc1..250a2e333 100644 --- a/packages/example-react/package.json +++ b/packages/example-react/package.json @@ -1,7 +1,7 @@ { "name": "example-react", "private": true, - "version": "2.19.0", + "version": "2.19.1", "scripts": { "start": "vite --open", "build": "tsc && vite build", @@ -9,7 +9,7 @@ }, "dependencies": { "@livechat/design-system-icons": "^2.19.0", - "@livechat/design-system-react-components": "^2.19.0", + "@livechat/design-system-react-components": "^2.19.1", "react": "^18.3.0", "react-dom": "^18.3.0" }, diff --git a/packages/react-components/package.json b/packages/react-components/package.json index f44cb506e..1c1ed15eb 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@livechat/design-system-react-components", - "version": "2.19.0", + "version": "2.19.1", "description": "", "publishConfig": { "access": "public" From 7e30f8b78d434174f6b30ca0e44b23fe286f96fc Mon Sep 17 00:00:00 2001 From: Joanna S <37884374+JoannaSikora@users.noreply.github.com> Date: Fri, 31 Jan 2025 15:19:56 +0100 Subject: [PATCH 06/11] feat(Icon): add shortcuts icons (#1511) --- packages/icons/svg/ShortcutArrowBack.svg | 3 + packages/icons/svg/ShortcutArrowDown.svg | 3 + packages/icons/svg/ShortcutArrowRight.svg | 3 + packages/icons/svg/ShortcutArrowUp.svg | 3 + packages/icons/svg/ShortcutCapsLock.svg | 4 + packages/icons/svg/ShortcutCommand.svg | 3 + packages/icons/svg/ShortcutControl.svg | 3 + packages/icons/svg/ShortcutDelete.svg | 3 + packages/icons/svg/ShortcutEnter.svg | 3 + packages/icons/svg/ShortcutOption.svg | 3 + packages/icons/svg/ShortcutShift.svg | 3 + packages/icons/svg/ShortcutTab.svg | 3 + .../src/components/Icon/Icon.stories.tsx | 88 +++++++++++++++++++ .../components/Icon/IconsShowcase/constans.ts | 12 +++ 14 files changed, 137 insertions(+) create mode 100644 packages/icons/svg/ShortcutArrowBack.svg create mode 100644 packages/icons/svg/ShortcutArrowDown.svg create mode 100644 packages/icons/svg/ShortcutArrowRight.svg create mode 100644 packages/icons/svg/ShortcutArrowUp.svg create mode 100644 packages/icons/svg/ShortcutCapsLock.svg create mode 100644 packages/icons/svg/ShortcutCommand.svg create mode 100644 packages/icons/svg/ShortcutControl.svg create mode 100644 packages/icons/svg/ShortcutDelete.svg create mode 100644 packages/icons/svg/ShortcutEnter.svg create mode 100644 packages/icons/svg/ShortcutOption.svg create mode 100644 packages/icons/svg/ShortcutShift.svg create mode 100644 packages/icons/svg/ShortcutTab.svg diff --git a/packages/icons/svg/ShortcutArrowBack.svg b/packages/icons/svg/ShortcutArrowBack.svg new file mode 100644 index 000000000..1a8d9dfaa --- /dev/null +++ b/packages/icons/svg/ShortcutArrowBack.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutArrowDown.svg b/packages/icons/svg/ShortcutArrowDown.svg new file mode 100644 index 000000000..bf162bc4d --- /dev/null +++ b/packages/icons/svg/ShortcutArrowDown.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutArrowRight.svg b/packages/icons/svg/ShortcutArrowRight.svg new file mode 100644 index 000000000..baf200aae --- /dev/null +++ b/packages/icons/svg/ShortcutArrowRight.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutArrowUp.svg b/packages/icons/svg/ShortcutArrowUp.svg new file mode 100644 index 000000000..bd02c6249 --- /dev/null +++ b/packages/icons/svg/ShortcutArrowUp.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutCapsLock.svg b/packages/icons/svg/ShortcutCapsLock.svg new file mode 100644 index 000000000..ff4095284 --- /dev/null +++ b/packages/icons/svg/ShortcutCapsLock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/icons/svg/ShortcutCommand.svg b/packages/icons/svg/ShortcutCommand.svg new file mode 100644 index 000000000..b5038e481 --- /dev/null +++ b/packages/icons/svg/ShortcutCommand.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutControl.svg b/packages/icons/svg/ShortcutControl.svg new file mode 100644 index 000000000..3a8585402 --- /dev/null +++ b/packages/icons/svg/ShortcutControl.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutDelete.svg b/packages/icons/svg/ShortcutDelete.svg new file mode 100644 index 000000000..ce858d14b --- /dev/null +++ b/packages/icons/svg/ShortcutDelete.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutEnter.svg b/packages/icons/svg/ShortcutEnter.svg new file mode 100644 index 000000000..228cae9fa --- /dev/null +++ b/packages/icons/svg/ShortcutEnter.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutOption.svg b/packages/icons/svg/ShortcutOption.svg new file mode 100644 index 000000000..633cb1cc5 --- /dev/null +++ b/packages/icons/svg/ShortcutOption.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutShift.svg b/packages/icons/svg/ShortcutShift.svg new file mode 100644 index 000000000..5c251a33d --- /dev/null +++ b/packages/icons/svg/ShortcutShift.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/icons/svg/ShortcutTab.svg b/packages/icons/svg/ShortcutTab.svg new file mode 100644 index 000000000..ff62773ac --- /dev/null +++ b/packages/icons/svg/ShortcutTab.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/react-components/src/components/Icon/Icon.stories.tsx b/packages/react-components/src/components/Icon/Icon.stories.tsx index 41b7cb88f..ad8cbf762 100644 --- a/packages/react-components/src/components/Icon/Icon.stories.tsx +++ b/packages/react-components/src/components/Icon/Icon.stories.tsx @@ -4,11 +4,75 @@ import * as TablerIcons from '@livechat/design-system-icons'; import { Meta } from '@storybook/react'; import { StoryDescriptor } from '../../stories/components/StoryDescriptor'; +import { Tag } from '../Tag'; import { Icon, IconProps } from './Icon'; const iterator = Object.keys(TablerIcons); +const SHORTCUT_ITEMS = [ + { + icon: TablerIcons.ShortcutCommand, + text: 'Command', + unicode: '⌘', + }, + { + icon: TablerIcons.ShortcutOption, + text: 'Option', + unicode: '⌥ ', + }, + { + icon: TablerIcons.ShortcutControl, + text: 'Control', + unicode: '⌃', + }, + { + icon: TablerIcons.ShortcutShift, + text: 'Shift', + unicode: '⇧', + }, + { + icon: TablerIcons.ShortcutTab, + text: 'Tab', + unicode: '⇥', + }, + { + icon: TablerIcons.ShortcutCapsLock, + text: 'Caps Lock', + unicode: '⇪', + }, + { + icon: TablerIcons.ShortcutDelete, + text: 'Delete', + unicode: '⌫', + }, + { + icon: TablerIcons.ShortcutEnter, + text: 'Enter', + unicode: '↵', + }, + { + icon: TablerIcons.ShortcutArrowUp, + text: 'Arrow up', + unicode: '↑', + }, + { + icon: TablerIcons.ShortcutArrowDown, + text: 'Arrow down', + unicode: '↓', + }, + { + icon: TablerIcons.ShortcutArrowBack, + text: 'Arrow left', + unicode: '←', + }, + { + icon: TablerIcons.ShortcutArrowRight, + text: 'Arrow right', + unicode: '→', + }, +]; + export default { title: 'Components/Icon', component: Icon, @@ -76,3 +140,27 @@ export const Sizes = (): React.ReactElement => (
    ); + +export const ShortcutsExamples = (): React.ReactElement => ( +
    + +
    + {SHORTCUT_ITEMS.map((item) => ( + } key={item.text}> + {item.text} + + ))} +
    +
    + + +
    + {SHORTCUT_ITEMS.map((item) => ( + + {item.unicode} {item.text} + + ))} +
    +
    +
    +); diff --git a/packages/react-components/src/components/Icon/IconsShowcase/constans.ts b/packages/react-components/src/components/Icon/IconsShowcase/constans.ts index 5b67df9c7..a589a2c2b 100644 --- a/packages/react-components/src/components/Icon/IconsShowcase/constans.ts +++ b/packages/react-components/src/components/Icon/IconsShowcase/constans.ts @@ -462,6 +462,18 @@ export const IconsData: Record = { LayoutSidebarLeft: IconGroup.General, SquareRoundedPlusFilled: IconGroup.General, SquareRoundedPlus: IconGroup.General, + ShortcutCommand: IconGroup.General, + ShortcutOption: IconGroup.General, + ShortcutShift: IconGroup.General, + ShortcutTab: IconGroup.General, + ShortcutCapsLock: IconGroup.General, + ShortcutControl: IconGroup.General, + ShortcutDelete: IconGroup.General, + ShortcutEnter: IconGroup.General, + ShortcutArrowUp: IconGroup.General, + ShortcutArrowDown: IconGroup.General, + ShortcutArrowBack: IconGroup.General, + ShortcutArrowRight: IconGroup.General, //Brands ChipCopilotColored: IconGroup.Brands, From 801d884958ffcb8b6daf84e98c0544a119ac8bb1 Mon Sep 17 00:00:00 2001 From: joannasikora Date: Fri, 31 Jan 2025 15:21:25 +0100 Subject: [PATCH 07/11] v2.20.0 --- lerna.json | 2 +- package-lock.json | 12 ++++++------ packages/example-react/package.json | 6 +++--- packages/icons/package.json | 2 +- packages/react-components/package.json | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lerna.json b/lerna.json index a4cbe1590..da73375fa 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "packages": ["packages/*"], - "version": "2.19.1", + "version": "2.20.0", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } diff --git a/package-lock.json b/package-lock.json index 6e2421174..10a1a2770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25990,10 +25990,10 @@ } }, "packages/example-react": { - "version": "2.19.1", + "version": "2.20.0", "dependencies": { - "@livechat/design-system-icons": "^2.19.0", - "@livechat/design-system-react-components": "^2.19.1", + "@livechat/design-system-icons": "^2.20.0", + "@livechat/design-system-react-components": "^2.20.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, @@ -26007,7 +26007,7 @@ }, "packages/icons": { "name": "@livechat/design-system-icons", - "version": "2.19.0", + "version": "2.20.0", "devDependencies": { "@svgr/cli": "^8.1.0", "glob": "^10.3.10", @@ -26029,12 +26029,12 @@ }, "packages/react-components": { "name": "@livechat/design-system-react-components", - "version": "2.19.1", + "version": "2.20.0", "license": "ISC", "dependencies": { "@floating-ui/react": "^0.26.25", "@livechat/data-utils": "^0.2.16", - "@livechat/design-system-icons": "^2.19.0", + "@livechat/design-system-icons": "^2.20.0", "clsx": "^1.1.1", "date-fns": "^2.28.0", "lodash.debounce": "^4.0.8", diff --git a/packages/example-react/package.json b/packages/example-react/package.json index 250a2e333..4e1d93011 100644 --- a/packages/example-react/package.json +++ b/packages/example-react/package.json @@ -1,15 +1,15 @@ { "name": "example-react", "private": true, - "version": "2.19.1", + "version": "2.20.0", "scripts": { "start": "vite --open", "build": "tsc && vite build", "preview": "vite preview" }, "dependencies": { - "@livechat/design-system-icons": "^2.19.0", - "@livechat/design-system-react-components": "^2.19.1", + "@livechat/design-system-icons": "^2.20.0", + "@livechat/design-system-react-components": "^2.20.0", "react": "^18.3.0", "react-dom": "^18.3.0" }, diff --git a/packages/icons/package.json b/packages/icons/package.json index 37c3bfc6c..fda9d5ed5 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@livechat/design-system-icons", - "version": "2.19.0", + "version": "2.20.0", "description": "", "publishConfig": { "access": "public" diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 1c1ed15eb..d1a8499a2 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -1,6 +1,6 @@ { "name": "@livechat/design-system-react-components", - "version": "2.19.1", + "version": "2.20.0", "description": "", "publishConfig": { "access": "public" @@ -76,7 +76,7 @@ "dependencies": { "@floating-ui/react": "^0.26.25", "@livechat/data-utils": "^0.2.16", - "@livechat/design-system-icons": "^2.19.0", + "@livechat/design-system-icons": "^2.20.0", "clsx": "^1.1.1", "date-fns": "^2.28.0", "lodash.debounce": "^4.0.8", From 5ef4816db612107158d96804f282079dca6b8107 Mon Sep 17 00:00:00 2001 From: Joanna S <37884374+JoannaSikora@users.noreply.github.com> Date: Mon, 3 Feb 2025 08:00:23 +0100 Subject: [PATCH 08/11] fix(Button): fix Button text wrapping (#1506) --- packages/react-components/src/components/Button/styles/base.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-components/src/components/Button/styles/base.scss b/packages/react-components/src/components/Button/styles/base.scss index e280868c5..0f6dbf936 100644 --- a/packages/react-components/src/components/Button/styles/base.scss +++ b/packages/react-components/src/components/Button/styles/base.scss @@ -14,6 +14,7 @@ text-align: center; text-decoration: none; line-height: 20px; + white-space: nowrap; font-size: 14px; font-weight: 600; user-select: none; From be88a3ea30b6c02c3cc69ed93c0ad0cc3eb09284 Mon Sep 17 00:00:00 2001 From: Marcin Sawicki Date: Mon, 3 Feb 2025 08:46:21 +0100 Subject: [PATCH 09/11] feat(Accordion): moved to emotion styles (#1470) --- package-lock.json | 170 ++++++++++++++++++ packages/react-components/package.json | 4 +- .../Accordion/Accordion.module.scss | 116 ------------ .../src/components/Accordion/Accordion.tsx | 37 ++-- .../AccordionAnimatedLabel.module.scss | 21 --- .../AccordionAnimatedLabel.spec.tsx | 2 +- .../AccordionAnimatedLabel.tsx | 29 +-- .../AccordionAnimatedLabel/styles.ts | 33 ++++ .../AccordionMultilineElement.module.scss | 11 -- .../AccordionMultilineElement.tsx | 12 +- .../AccordionMultilineElement/styles.ts | 14 ++ .../components/Accordion/components/index.ts | 2 + .../src/components/Accordion/helpers.tsx | 2 +- .../src/components/Accordion/styles.ts | 146 +++++++++++++++ 14 files changed, 391 insertions(+), 208 deletions(-) delete mode 100644 packages/react-components/src/components/Accordion/Accordion.module.scss delete mode 100644 packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.module.scss rename packages/react-components/src/components/Accordion/components/{ => AccordionAnimatedLabel}/AccordionAnimatedLabel.spec.tsx (94%) rename packages/react-components/src/components/Accordion/components/{ => AccordionAnimatedLabel}/AccordionAnimatedLabel.tsx (59%) create mode 100644 packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/styles.ts delete mode 100644 packages/react-components/src/components/Accordion/components/AccordionMultilineElement.module.scss rename packages/react-components/src/components/Accordion/components/{ => AccordionMultilineElement}/AccordionMultilineElement.tsx (67%) create mode 100644 packages/react-components/src/components/Accordion/components/AccordionMultilineElement/styles.ts create mode 100644 packages/react-components/src/components/Accordion/components/index.ts create mode 100644 packages/react-components/src/components/Accordion/styles.ts diff --git a/package-lock.json b/package-lock.json index 10a1a2770..31932dbc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2543,6 +2543,103 @@ "node": ">=10.0.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dev": true, + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "dev": true, + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true + }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", @@ -2552,6 +2649,18 @@ "react": ">=16.8.0" } }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -7835,6 +7944,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, "node_modules/@types/pretty-hrtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", @@ -9134,6 +9249,46 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -13284,6 +13439,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -23258,6 +23419,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "dev": true + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -26032,6 +26199,7 @@ "version": "2.20.0", "license": "ISC", "dependencies": { + "@emotion/css": "^11.13.0", "@floating-ui/react": "^0.26.25", "@livechat/data-utils": "^0.2.16", "@livechat/design-system-icons": "^2.20.0", @@ -26046,6 +26214,7 @@ }, "devDependencies": { "@chromatic-com/storybook": "^1.5.0", + "@emotion/css": "^11.13.0", "@storybook/addon-a11y": "^8.1.7", "@storybook/addon-actions": "^8.1.7", "@storybook/addon-backgrounds": "^8.1.7", @@ -26083,6 +26252,7 @@ "vitest": "^1.6.0" }, "peerDependencies": { + "@emotion/css": "^11.13.0", "react": ">= 18.3.0", "react-dom": ">= 18.3.0" } diff --git a/packages/react-components/package.json b/packages/react-components/package.json index d1a8499a2..c9574f2db 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -33,7 +33,8 @@ }, "peerDependencies": { "react": ">= 18.3.0", - "react-dom": ">= 18.3.0" + "react-dom": ">= 18.3.0", + "@emotion/css": "^11.13.0" }, "devDependencies": { "@chromatic-com/storybook": "^1.5.0", @@ -74,6 +75,7 @@ "vitest": "^1.6.0" }, "dependencies": { + "@emotion/css": "^11.13.0", "@floating-ui/react": "^0.26.25", "@livechat/data-utils": "^0.2.16", "@livechat/design-system-icons": "^2.20.0", diff --git a/packages/react-components/src/components/Accordion/Accordion.module.scss b/packages/react-components/src/components/Accordion/Accordion.module.scss deleted file mode 100644 index 598302138..000000000 --- a/packages/react-components/src/components/Accordion/Accordion.module.scss +++ /dev/null @@ -1,116 +0,0 @@ -$base-class: 'accordion'; - -.#{$base-class} { - display: flex; - position: relative; - flex-direction: column; - justify-content: space-between; - transition: all var(--transition-duration-moderate-1); - border: 1px solid transparent; - border-radius: var(--radius-4); - box-shadow: unset; - background-color: var(--surface-secondary-default); - width: 100%; - min-height: 24px; - - &:focus-visible { - outline: 0; - box-shadow: var(--shadow-focus); - } - - &:hover { - border-color: var(--border-basic-hover); - box-shadow: var(--shadow-float); - } - - &--warning { - background-color: var(--surface-accent-emphasis-min-warning); - - &:hover { - border-color: var(--border-basic-warning); - } - } - - &--error { - background-color: var(--surface-accent-emphasis-min-negative); - - &:hover { - border-color: var(--content-basic-negative); - } - } - - &--promo { - border: 1px solid var(--border-basic-secondary); - background-color: var(--surface-primary-default); - } - - &--open { - border: 1px solid var(--action-primary-default); - box-shadow: var(--shadow-float); - background-color: var(--surface-primary-default); - - &:hover { - border-color: var(--action-primary-default); - } - } - - &__chevron { - position: absolute; - top: 22px; - right: 20px; - transition: inherit; - pointer-events: none; - - &--open { - transform: rotate(180deg); - } - - &--promo { - top: 26px; - } - } - - &__label { - margin: 0; - padding: var(--spacing-5) var(--spacing-12) var(--spacing-5) - var(--spacing-5); - - &:hover { - cursor: pointer; - } - - &--promo { - padding: var(--spacing-6) var(--spacing-12) var(--spacing-6) - var(--spacing-6); - } - } - - &__content { - transition: inherit; - height: 100%; - overflow: hidden; - - &__inner { - transition: all var(--transition-duration-moderate-1); - opacity: 0; - padding: 0 var(--spacing-12) var(--spacing-5) var(--spacing-5); - - &--open { - opacity: 1; - } - - &--promo { - padding: 0 var(--spacing-12) var(--spacing-6) var(--spacing-6); - } - } - } - - &__footer { - border-top: 1px solid var(--border-basic-secondary); - padding: var(--spacing-5); - - &--promo { - padding: var(--spacing-6); - } - } -} diff --git a/packages/react-components/src/components/Accordion/Accordion.tsx b/packages/react-components/src/components/Accordion/Accordion.tsx index a68b86a7d..de4a6a729 100644 --- a/packages/react-components/src/components/Accordion/Accordion.tsx +++ b/packages/react-components/src/components/Accordion/Accordion.tsx @@ -1,23 +1,22 @@ import * as React from 'react'; +import { cx } from '@emotion/css'; import { ChevronDown } from '@livechat/design-system-icons'; -import cx from 'clsx'; import { useAnimations, useHeightResizer } from '../../hooks'; import { Icon } from '../Icon'; import { Heading, Text, TTextSize } from '../Typography'; -import { AccordionMultilineElement } from './components/AccordionMultilineElement'; +import { AccordionMultilineElement } from './components'; import { getLabel } from './helpers'; import { useAccordion } from './hooks'; +import * as styles from './styles'; import { IAccordionProps, IAccordionPromoProps, IAccordionComponentProps, } from './types'; -import styles from './Accordion.module.scss'; - const AccordionComponent: React.FC = ({ className, mainClassName, @@ -54,7 +53,7 @@ const AccordionComponent: React.FC = ({ const mergedClassName = cx( mainClassName, { - [styles[`${baseClass}--open`]]: isExpanded, + [styles.open]: isExpanded, }, className ); @@ -64,9 +63,7 @@ const AccordionComponent: React.FC = ({ const props = { 'aria-expanded': isExpanded, as: 'div', - className: cx(styles[`${baseClass}__label`], { - [styles[`${baseClass}__label--promo`]]: isPromo, - }), + className: styles.label(isPromo), onClick: () => handleExpandChange(isExpanded), bold: !isPromo ? true : undefined, ...(isPromo ? { size: 'xs' as TTextSize } : {}), @@ -85,10 +82,7 @@ const AccordionComponent: React.FC = ({ > {buildHeader(isPromo)} {multilineElement && ( @@ -97,7 +91,7 @@ const AccordionComponent: React.FC = ({ )}
    @@ -106,10 +100,7 @@ const AccordionComponent: React.FC = ({ <> {children} @@ -118,9 +109,7 @@ const AccordionComponent: React.FC = ({ as="div" aria-label="Accordion footer" role="complementary" - className={cx(styles[`${baseClass}__footer`], { - [styles[`${baseClass}__footer--promo`]]: isPromo, - })} + className={styles.footer(isPromo)} > {footer} @@ -133,18 +122,14 @@ const AccordionComponent: React.FC = ({ ); }; -const baseClass = 'accordion'; - export const Accordion: React.FC = ({ kind, ...props }) => { - const mainClassName = cx(styles[baseClass], styles[`${baseClass}--${kind}`]); + const mainClassName = cx(styles.baseStyles(), styles.kind(kind)); return ; }; -const promoBaseClass = `${baseClass}--promo`; - export const AccordionPromo: React.FC = (props) => { - const mainClassName = cx(styles[baseClass], styles[promoBaseClass]); + const mainClassName = cx(styles.baseStyles(true)); return ( diff --git a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.module.scss b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.module.scss deleted file mode 100644 index 7901f5207..000000000 --- a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.module.scss +++ /dev/null @@ -1,21 +0,0 @@ -$base-class: 'accordion-animated-label'; - -.#{$base-class} { - display: flex; - position: relative; - align-items: center; - transition: all var(--transition-duration-fast-2) ease-in-out; - min-height: 24px; - - &__open, - &__close { - position: absolute; - transition: all var(--transition-duration-fast-2) ease-in-out; - opacity: 0; - max-width: 100%; - - &--visible { - opacity: 1; - } - } -} diff --git a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.spec.tsx b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/AccordionAnimatedLabel.spec.tsx similarity index 94% rename from packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.spec.tsx rename to packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/AccordionAnimatedLabel.spec.tsx index 4a4b39f39..3e1af9771 100644 --- a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.spec.tsx +++ b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/AccordionAnimatedLabel.spec.tsx @@ -1,6 +1,6 @@ import { render } from 'test-utils'; -import { IAccordionAnimatedLabelProps } from '../types'; +import { IAccordionAnimatedLabelProps } from '../../types'; import { AccordionAnimatedLabel } from './AccordionAnimatedLabel'; diff --git a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.tsx b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/AccordionAnimatedLabel.tsx similarity index 59% rename from packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.tsx rename to packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/AccordionAnimatedLabel.tsx index 48c57d486..8ad06200e 100644 --- a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel.tsx +++ b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/AccordionAnimatedLabel.tsx @@ -1,13 +1,9 @@ import * as React from 'react'; -import cx from 'clsx'; +import { useAnimations } from '../../../../hooks'; +import { IAccordionAnimatedLabelProps } from '../../types'; -import { useAnimations } from '../../../hooks'; -import { IAccordionAnimatedLabelProps } from '../types'; - -import styles from './AccordionAnimatedLabel.module.scss'; - -const baseClass = `accordion-animated-label`; +import * as styles from './styles'; export const AccordionAnimatedLabel: React.FC = ({ open, @@ -39,27 +35,14 @@ export const AccordionAnimatedLabel: React.FC = ({ }, [isOpen, isOpenMounted, isClosedMounted]); return ( -
    +
    {isOpenMounted && ( -
    +
    {open}
    )} {isClosedMounted && ( -
    +
    {closed}
    )} diff --git a/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/styles.ts b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/styles.ts new file mode 100644 index 000000000..130381fd1 --- /dev/null +++ b/packages/react-components/src/components/Accordion/components/AccordionAnimatedLabel/styles.ts @@ -0,0 +1,33 @@ +import { css } from '@emotion/css'; + +import { TransitionDurationToken } from '../../../../foundations'; + +export const baseStyles = (containerHeight?: number) => css` + display: flex; + position: relative; + align-items: center; + transition: all var(${TransitionDurationToken.Fast2}) ease-in-out; + min-height: 24px; + + ${containerHeight + ? ` + height: ${containerHeight}px; + ` + : ` + height: auto; + `} +`; + +export const element = (isVisible: boolean) => css` + position: absolute; + transition: all var(${TransitionDurationToken.Fast2}) ease-in-out; + max-width: 100%; + + ${isVisible + ? ` + opacity: 1; + ` + : ` + opacity: 0; + `} +`; diff --git a/packages/react-components/src/components/Accordion/components/AccordionMultilineElement.module.scss b/packages/react-components/src/components/Accordion/components/AccordionMultilineElement.module.scss deleted file mode 100644 index 3fd42a7c1..000000000 --- a/packages/react-components/src/components/Accordion/components/AccordionMultilineElement.module.scss +++ /dev/null @@ -1,11 +0,0 @@ -$base-class: 'accordion-multiline'; - -.#{$base-class} { - transition: inherit; - height: 100%; - overflow: hidden; - - &__inner { - padding: 0 var(--spacing-12) var(--spacing-5) var(--spacing-5); - } -} diff --git a/packages/react-components/src/components/Accordion/components/AccordionMultilineElement.tsx b/packages/react-components/src/components/Accordion/components/AccordionMultilineElement/AccordionMultilineElement.tsx similarity index 67% rename from packages/react-components/src/components/Accordion/components/AccordionMultilineElement.tsx rename to packages/react-components/src/components/Accordion/components/AccordionMultilineElement/AccordionMultilineElement.tsx index 8525b2cda..bbec0620b 100644 --- a/packages/react-components/src/components/Accordion/components/AccordionMultilineElement.tsx +++ b/packages/react-components/src/components/Accordion/components/AccordionMultilineElement/AccordionMultilineElement.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; -import { useAnimations, useHeightResizer } from '../../../hooks'; +import { useAnimations, useHeightResizer } from '../../../../hooks'; -import styles from './AccordionMultilineElement.module.scss'; - -const baseClass = 'accordion-multiline'; +import * as styles from './styles'; export interface IAccordionMultilineElementProps { children: React.ReactNode; @@ -23,14 +21,12 @@ export const AccordionMultilineElement: React.FC< return (
    - {isMounted && ( -
    {children}
    - )} + {isMounted &&
    {children}
    }
    ); diff --git a/packages/react-components/src/components/Accordion/components/AccordionMultilineElement/styles.ts b/packages/react-components/src/components/Accordion/components/AccordionMultilineElement/styles.ts new file mode 100644 index 000000000..de0af5b44 --- /dev/null +++ b/packages/react-components/src/components/Accordion/components/AccordionMultilineElement/styles.ts @@ -0,0 +1,14 @@ +import { css } from '@emotion/css'; + +import { SpacingToken } from '../../../../foundations'; + +export const baseStyles = css` + transition: inherit; + height: 100%; + overflow: hidden; +`; + +export const inner = css` + padding: 0 var(${SpacingToken.Spacing12}) var(${SpacingToken.Spacing5}) + var(${SpacingToken.Spacing5}); +`; diff --git a/packages/react-components/src/components/Accordion/components/index.ts b/packages/react-components/src/components/Accordion/components/index.ts new file mode 100644 index 000000000..776d07259 --- /dev/null +++ b/packages/react-components/src/components/Accordion/components/index.ts @@ -0,0 +1,2 @@ +export { AccordionAnimatedLabel } from './AccordionAnimatedLabel/AccordionAnimatedLabel'; +export { AccordionMultilineElement } from './AccordionMultilineElement/AccordionMultilineElement'; diff --git a/packages/react-components/src/components/Accordion/helpers.tsx b/packages/react-components/src/components/Accordion/helpers.tsx index 81d3abaff..eb97fcf0a 100644 --- a/packages/react-components/src/components/Accordion/helpers.tsx +++ b/packages/react-components/src/components/Accordion/helpers.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { AccordionAnimatedLabel } from './components/AccordionAnimatedLabel'; +import { AccordionAnimatedLabel } from './components'; import type { AccordionLabel } from './types'; diff --git a/packages/react-components/src/components/Accordion/styles.ts b/packages/react-components/src/components/Accordion/styles.ts new file mode 100644 index 000000000..8dbb8ecfa --- /dev/null +++ b/packages/react-components/src/components/Accordion/styles.ts @@ -0,0 +1,146 @@ +import { css } from '@emotion/css'; + +import { + DesignToken, + RadiusToken, + ShadowToken, + SpacingToken, + TransitionDurationToken, +} from '../../foundations'; + +export const baseStyles = (isPromo?: boolean) => css` + display: flex; + position: relative; + flex-direction: column; + justify-content: space-between; + transition: all var(${TransitionDurationToken.Moderate1}); + border-radius: var(${RadiusToken.Radius4}); + box-shadow: unset; + width: 100%; + min-height: 24px; + + ${isPromo + ? ` + border: 1px solid var(${DesignToken.BorderBasicSecondary}); + background-color: var(${DesignToken.SurfacePrimaryDefault}); + ` + : ` + border: 1px solid transparent; + background-color: var(${DesignToken.SurfaceSecondaryDefault}); + `} + + &:focus-visible { + outline: 0; + box-shadow: var(${ShadowToken.Focus}); + } + + &:hover { + border-color: var(${DesignToken.BorderBasicHover}); + box-shadow: var(${ShadowToken.Float}); + } +`; + +export const kind = (kind?: string) => css` + ${kind === 'warning' && + ` + background-color: var(${DesignToken.SurfaceAccentEmphasisMinWarning}); + + &:hover { + border-color: var(${DesignToken.BorderBasicWarning}); + } + `} + + ${kind === 'error' && + ` + background-color: var(${DesignToken.SurfaceAccentEmphasisMinNegative}); + + &:hover { + border-color: var(${DesignToken.ContentBasicNegative}); + } + `} +`; + +export const open = css` + border: 1px solid var(${DesignToken.ActionPrimaryDefault}); + box-shadow: var(${ShadowToken.Float}); + background-color: var(${DesignToken.SurfacePrimaryDefault}); + + &:hover { + border-color: var(${DesignToken.ActionPrimaryDefault}); + } +`; + +export const label = (isPromo?: boolean) => css` + margin: 0; + + ${isPromo + ? ` + padding: var(${SpacingToken.Spacing6}) var(${SpacingToken.Spacing12}) var(${SpacingToken.Spacing6}) var(${SpacingToken.Spacing6}); + ` + : ` + padding: var(${SpacingToken.Spacing5}) var(${SpacingToken.Spacing12}) var(${SpacingToken.Spacing5}) var(${SpacingToken.Spacing5}); + `} + + &:hover { + cursor: pointer; + } +`; + +export const chevron = (isOpen?: boolean, isPromo?: boolean) => css` + position: absolute; + right: 20px; + transition: inherit; + pointer-events: none; + + ${isPromo + ? ` + top: 26px; + ` + : ` + top: 22px; + `} + + ${isOpen && + ` + transform: rotate(180deg); + `} +`; + +export const content = css` + transition: inherit; + height: 100%; + overflow: hidden; +`; + +export const contentInner = (isOpen?: boolean, isPromo?: boolean) => css` + transition: all var(${TransitionDurationToken.Moderate1}); + + ${isOpen + ? ` + opacity: 1; + ` + : ` + opacity: 0; + `} + + ${isPromo + ? ` + padding: 0 var(${SpacingToken.Spacing12}) var(${SpacingToken.Spacing6}) var(${SpacingToken.Spacing6}); + ` + : ` + padding: 0 var(${SpacingToken.Spacing12}) var(${SpacingToken.Spacing5}) var(${SpacingToken.Spacing5}); + `} +`; + +export const footer = (isPromo?: boolean) => css` + border-top: 1px solid var(${DesignToken.BorderBasicSecondary}); + padding: var(${SpacingToken.Spacing5}); + + ${isPromo + ? ` + padding: var(${SpacingToken.Spacing6}); + ` + : ` + padding: var(${SpacingToken.Spacing5}); + `} +`; From 6c90ff8c1db4d3d782b4d29f945252e15aafc0d4 Mon Sep 17 00:00:00 2001 From: Joanna S <37884374+JoannaSikora@users.noreply.github.com> Date: Mon, 3 Feb 2025 09:13:01 +0100 Subject: [PATCH 10/11] feat(ActionCard): add loading state with optional animation (#1507) --- .../ActionCard/ActionCard.module.scss | 31 +++++++++ .../components/ActionCard/ActionCard.spec.tsx | 9 +++ .../ActionCard/ActionCard.stories.tsx | 18 ++++++ .../src/components/ActionCard/ActionCard.tsx | 64 ++++++++++++------- .../src/components/ActionCard/types.ts | 25 +++++++- .../components/Skeleton/Skeleton.module.scss | 36 ++--------- .../src/styles/_animations.scss | 29 +++++++++ 7 files changed, 154 insertions(+), 58 deletions(-) create mode 100644 packages/react-components/src/styles/_animations.scss diff --git a/packages/react-components/src/components/ActionCard/ActionCard.module.scss b/packages/react-components/src/components/ActionCard/ActionCard.module.scss index af8a6e432..58d886ab3 100644 --- a/packages/react-components/src/components/ActionCard/ActionCard.module.scss +++ b/packages/react-components/src/components/ActionCard/ActionCard.module.scss @@ -1,3 +1,5 @@ +@import '../../styles/animations'; + $base-class: 'action-card'; @mixin verticalStyles { @@ -11,8 +13,37 @@ $base-class: 'action-card'; } } +.visually-hidden { + position: absolute; + margin: -1px; + border: 0; + padding: 0; + width: 1px; + height: 1px; + overflow: hidden; + white-space: nowrap; + clip: rect(0, 0, 0, 0); +} + .main-wrapper { container-type: inline-size; + + &.#{$base-class}--loading { + border: 0; + border-radius: var(--radius-4); + background-color: var(--surface-secondary-disabled); + cursor: default; + width: 100%; + min-height: 280px; + + .#{$base-class} { + display: none; + } + } + + &.#{$base-class}--loading--animated { + @include skeleton-loading; + } } .#{$base-class} { diff --git a/packages/react-components/src/components/ActionCard/ActionCard.spec.tsx b/packages/react-components/src/components/ActionCard/ActionCard.spec.tsx index 073f641d8..e4b0c9277 100644 --- a/packages/react-components/src/components/ActionCard/ActionCard.spec.tsx +++ b/packages/react-components/src/components/ActionCard/ActionCard.spec.tsx @@ -73,4 +73,13 @@ describe(' component', () => { expect(onClick).not.toHaveBeenCalled(); expect(onButtonClick).toHaveBeenCalledTimes(1); }); + + it('should display loading state when isLoading is true', () => { + const { queryByText } = renderComponent({ + isLoading: true, + }); + + expect(queryByText('Example content')).toBeNull(); + expect(queryByText('Example button')).toBeNull(); + }); }); diff --git a/packages/react-components/src/components/ActionCard/ActionCard.stories.tsx b/packages/react-components/src/components/ActionCard/ActionCard.stories.tsx index afb30e705..9dffe6599 100644 --- a/packages/react-components/src/components/ActionCard/ActionCard.stories.tsx +++ b/packages/react-components/src/components/ActionCard/ActionCard.stories.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Meta } from '@storybook/react'; import image from '../../stories/assets/chat-window.png'; +import { StoryDescriptor } from '../../stories/components/StoryDescriptor'; import { Button } from '../Button'; import { Heading, Text } from '../Typography'; @@ -77,3 +78,20 @@ export const TwoColumns = (): React.ReactElement => { ); }; + +export const Loading = (): React.ReactElement => { + return ( + <> + + + + + + + + + + + + ); +}; diff --git a/packages/react-components/src/components/ActionCard/ActionCard.tsx b/packages/react-components/src/components/ActionCard/ActionCard.tsx index 5530d574c..24c7584f0 100644 --- a/packages/react-components/src/components/ActionCard/ActionCard.tsx +++ b/packages/react-components/src/components/ActionCard/ActionCard.tsx @@ -15,10 +15,18 @@ export const ActionCard: FC> = ({ firstColumnClassName, secondColumnClassName, onClick, + isLoading = false, + isLoadingAnimated = false, }) => { const mergedClassNames = cx(styles[baseClass], className); + const wrapperClassNames = cx(styles[`main-wrapper`], { + [styles[`${baseClass}--loading`]]: isLoading, + [styles[`${baseClass}--loading--animated`]]: isLoadingAnimated, + }); + const handleOnClick = (e: MouseEvent) => { + if (isLoading) return; if (e.currentTarget !== document.activeElement) { return; } @@ -27,6 +35,7 @@ export const ActionCard: FC> = ({ }; const handleOnKeyDown = (e: KeyboardEvent) => { + if (isLoading) return; if (e.currentTarget !== document.activeElement) { return; } @@ -38,42 +47,49 @@ export const ActionCard: FC> = ({ e.key === 'Space' ) { e.preventDefault(); - onClick?.(); } }; return ( -
    +
    +
    + {isLoading ? 'Loading content' : null} +
    -
    - {children} -
    - {secondColumn && ( -
    +
    + {children} +
    + {secondColumn && ( +
    + {secondColumn} +
    )} - > - {secondColumn} -
    + )}
    diff --git a/packages/react-components/src/components/ActionCard/types.ts b/packages/react-components/src/components/ActionCard/types.ts index 9d8321bc1..0b58f11c0 100644 --- a/packages/react-components/src/components/ActionCard/types.ts +++ b/packages/react-components/src/components/ActionCard/types.ts @@ -1,6 +1,27 @@ import * as React from 'react'; -export interface ActionCardProps { +export type ActionCardProps = ( + | { + /** + * Specify if the card is in loading state + */ + isLoading: true; + /** + * Specify if the card is in animated loading state + */ + isLoadingAnimated?: boolean; + } + | { + /** + * Specify if the card is in loading state + */ + isLoading?: false; + /** + * Loading animation is not available when not loading + */ + isLoadingAnimated?: never; + } +) & { /** * The CSS class for main container */ @@ -21,4 +42,4 @@ export interface ActionCardProps { * Optional handler called on card click */ onClick?: () => void; -} +}; diff --git a/packages/react-components/src/components/Skeleton/Skeleton.module.scss b/packages/react-components/src/components/Skeleton/Skeleton.module.scss index d80ebd688..13883319d 100644 --- a/packages/react-components/src/components/Skeleton/Skeleton.module.scss +++ b/packages/react-components/src/components/Skeleton/Skeleton.module.scss @@ -1,39 +1,11 @@ +@import '../../styles/animations'; + $base-class: 'skeleton'; @mixin background() { background-color: var(--surface-secondary-disabled); } -@keyframes loading { - 0% { - left: -100%; - } - - 100% { - left: 100%; - } -} - -@mixin animation() { - position: relative; - overflow: hidden; - - &::before { - position: absolute; - left: 0; - background: linear-gradient( - 90deg, - var(--animated-gradient-value-1), - var(--animated-gradient-value-2), - var(--animated-gradient-value-3) - ); - width: 100%; - height: 100%; - animation: loading 2s forwards infinite; - content: ''; - } -} - .skeleton-wrapper { display: flex; gap: var(--spacing-2); @@ -55,7 +27,7 @@ $base-class: 'skeleton'; } &--animated { - @include animation; + @include skeleton-loading; } } @@ -66,6 +38,6 @@ $base-class: 'skeleton'; width: 100%; &--animated { - @include animation; + @include skeleton-loading; } } diff --git a/packages/react-components/src/styles/_animations.scss b/packages/react-components/src/styles/_animations.scss new file mode 100644 index 000000000..8a89adb0b --- /dev/null +++ b/packages/react-components/src/styles/_animations.scss @@ -0,0 +1,29 @@ +@keyframes loading { + 0% { + left: -100%; + } + + 100% { + left: 100%; + } +} + +@mixin skeleton-loading { + position: relative; + overflow: hidden; + + &::before { + position: absolute; + left: 0; + background: linear-gradient( + 90deg, + var(--animated-gradient-value-1), + var(--animated-gradient-value-2), + var(--animated-gradient-value-3) + ); + width: 100%; + height: 100%; + animation: loading 2s forwards infinite; + content: ''; + } +} From 69030e058781ef244338c79afba9edc11a589fe4 Mon Sep 17 00:00:00 2001 From: Joanna S <37884374+JoannaSikora@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:26:55 +0100 Subject: [PATCH 11/11] feat(Picker): add support for toggling dropdown via chevron icon and input field (#1509) --- .../src/components/Picker/Picker.spec.tsx | 22 ++++++++++++++++++- .../src/components/Picker/Picker.tsx | 1 + .../Picker/components/PickerTrigger.spec.tsx | 1 + .../Picker/components/PickerTrigger.tsx | 13 +++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/react-components/src/components/Picker/Picker.spec.tsx b/packages/react-components/src/components/Picker/Picker.spec.tsx index dff819eaa..97f6ca7f1 100644 --- a/packages/react-components/src/components/Picker/Picker.spec.tsx +++ b/packages/react-components/src/components/Picker/Picker.spec.tsx @@ -89,6 +89,26 @@ describe(' component', () => { expect(onClose).not.toHaveBeenCalled(); // because it was not visible }); + it('should toggle dropdown when clicking on the input or arrow', () => { + const onOpen = vi.fn(); + const onClose = vi.fn(); + const { getByTestId, queryByTestId, getByRole } = renderComponent({ + ...defaultProps, + onOpen, + onClose, + }); + + userEvent.click(getByTestId('picker-trigger__chevron-icon')); + + expect(onOpen).toHaveBeenCalled(); + expect(getByTestId('picker-list')).toBeVisible(); + + userEvent.click(getByRole('textbox')); + + expect(onClose).toHaveBeenCalled(); + expect(queryByTestId('picker-list')).toBeNull(); + }); + it('should call onSelect includes the currently selected options in multiselect mode', () => { const onSelect = vi.fn(); const onClose = vi.fn(); @@ -188,10 +208,10 @@ describe(' component', () => { it('should render given text for search empty state if no search result found', () => { const { getByText, getByRole } = renderComponent({ ...defaultProps, + isVisible: true, noSearchResultText: 'No results found', }); - userEvent.click(getByText('Select option')); userEvent.type(getByRole('textbox'), 'not existing option'); expect(getByText('No results found')).toBeVisible(); }); diff --git a/packages/react-components/src/components/Picker/Picker.tsx b/packages/react-components/src/components/Picker/Picker.tsx index ffc277e0f..9554802fb 100644 --- a/packages/react-components/src/components/Picker/Picker.tsx +++ b/packages/react-components/src/components/Picker/Picker.tsx @@ -145,6 +145,7 @@ export const Picker: React.FC = ({ isMultiSelect={type === 'multi'} size={size} setTriggerFocus={setTriggerFocus} + onToggle={() => handleVisibilityChange(!isOpen)} > noop, setTriggerFocus: () => noop, + onToggle: () => noop, }; const renderComponent = (props: PickerTriggerProps) => diff --git a/packages/react-components/src/components/Picker/components/PickerTrigger.tsx b/packages/react-components/src/components/Picker/components/PickerTrigger.tsx index 7d2957e06..f267bdef6 100644 --- a/packages/react-components/src/components/Picker/components/PickerTrigger.tsx +++ b/packages/react-components/src/components/Picker/components/PickerTrigger.tsx @@ -25,6 +25,7 @@ export interface PickerTriggerProps { isOpen: boolean; onClear: () => void; setTriggerFocus: (v: boolean) => void; + onToggle: () => void; } const baseClass = 'picker-trigger'; @@ -46,6 +47,7 @@ export const PickerTrigger: React.FC< onClear, children, setTriggerFocus, + onToggle, }) => { const mergedClassNames = cx( styles[baseClass], @@ -67,6 +69,15 @@ export const PickerTrigger: React.FC< onClear(); }; + const handleClick = (e: React.MouseEvent | React.KeyboardEvent) => { + if ( + !isDisabled && + !(isOpen && (e.target as HTMLElement).tagName === 'BUTTON') + ) { + onToggle(); + } + }; + React.useEffect(() => { if (!isOpen) setTriggerFocus(false); }, [isOpen]); @@ -83,6 +94,7 @@ export const PickerTrigger: React.FC< {...getReferenceProps()} onFocus={() => setTriggerFocus(true)} onBlur={() => setTriggerFocus(false)} + onClick={handleClick} > )}