From 5c22cc778358bfba48d3a1026c9af0cbae734f77 Mon Sep 17 00:00:00 2001 From: Bogdan Bosca <109202804+endv-bogdanb@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:01:28 +0300 Subject: [PATCH] feat(toast): add new `` component (#301) --- packages/bee-q/src/components.d.ts | 70 ++++++ packages/bee-q/src/components/icon/readme.md | 2 + .../toast/__tests__/bq-toast.e2e.ts | 100 ++++++++ .../components/toast/_storybook/bq-toast.mdx | 19 ++ .../toast/_storybook/bq-toast.stories.tsx | 138 +++++++++++ .../bee-q/src/components/toast/bq-toast.tsx | 225 ++++++++++++++++++ .../src/components/toast/bq-toast.types.ts | 12 + packages/bee-q/src/components/toast/readme.md | 86 +++++++ .../src/components/toast/scss/bq-toast.scss | 44 ++++ .../toast/scss/bq-toast.variables.scss | 42 ++++ .../src/global/styles/utils/_utility.scss | 34 +++ tailwind.config.ts | 14 ++ 12 files changed, 786 insertions(+) create mode 100644 packages/bee-q/src/components/toast/__tests__/bq-toast.e2e.ts create mode 100644 packages/bee-q/src/components/toast/_storybook/bq-toast.mdx create mode 100644 packages/bee-q/src/components/toast/_storybook/bq-toast.stories.tsx create mode 100644 packages/bee-q/src/components/toast/bq-toast.tsx create mode 100644 packages/bee-q/src/components/toast/bq-toast.types.ts create mode 100644 packages/bee-q/src/components/toast/readme.md create mode 100644 packages/bee-q/src/components/toast/scss/bq-toast.scss create mode 100644 packages/bee-q/src/components/toast/scss/bq-toast.variables.scss diff --git a/packages/bee-q/src/components.d.ts b/packages/bee-q/src/components.d.ts index ea5bd72d9..1deb7a3ac 100644 --- a/packages/bee-q/src/components.d.ts +++ b/packages/bee-q/src/components.d.ts @@ -19,6 +19,7 @@ import { TSpinnerSize, TSpinnerTextPosition } from "./components/spinner/bq-spin import { TStatusType } from "./components/status/bq-status.types"; import { TSwitchInnerLabel, TSwitchJustifyContent } from "./components/switch/bq-swithc.types"; import { TTabSize } from "./components/tab/bq-tab.types"; +import { TToastPlacement, TToastType } from "./components/toast/bq-toast.types"; import { FloatingUIPlacement } from "./services/interfaces"; export { TAvatarShape, TAvatarSize } from "./components/avatar/bq-avatar.types"; export { TBadgeSize } from "./components/badge/bq-badge.types"; @@ -34,6 +35,7 @@ export { TSpinnerSize, TSpinnerTextPosition } from "./components/spinner/bq-spin export { TStatusType } from "./components/status/bq-status.types"; export { TSwitchInnerLabel, TSwitchJustifyContent } from "./components/switch/bq-swithc.types"; export { TTabSize } from "./components/tab/bq-tab.types"; +export { TToastPlacement, TToastType } from "./components/toast/bq-toast.types"; export { FloatingUIPlacement } from "./services/interfaces"; export namespace Components { /** @@ -596,6 +598,31 @@ export namespace Components { */ "value": string; } + interface BqToast { + "hide": () => Promise; + /** + * If true will hide toast icon + */ + "hideIcon": boolean; + /** + * If true, the toast will be shown + */ + "open": boolean; + /** + * Placement of toast + */ + "placement": TToastPlacement; + "show": () => Promise; + /** + * The length of time, in milliseconds, after which the toast will close itself + */ + "time": number; + "toast": () => Promise; + /** + * Type of toast + */ + "type": TToastType; + } interface BqTooltip { /** * Set the action when the tooltip should be displayed, on hover (default) or click @@ -680,6 +707,10 @@ export interface BqTabGroupCustomEvent extends CustomEvent { detail: T; target: HTMLBqTabGroupElement; } +export interface BqToastCustomEvent extends CustomEvent { + detail: T; + target: HTMLBqToastElement; +} declare global { /** * An avatar represents an object made of different pieces of information, in a way that is understandable at a glance. @@ -805,6 +836,12 @@ declare global { prototype: HTMLBqTabGroupElement; new (): HTMLBqTabGroupElement; }; + interface HTMLBqToastElement extends Components.BqToast, HTMLStencilElement { + } + var HTMLBqToastElement: { + prototype: HTMLBqToastElement; + new (): HTMLBqToastElement; + }; interface HTMLBqTooltipElement extends Components.BqTooltip, HTMLStencilElement { } var HTMLBqTooltipElement: { @@ -830,6 +867,7 @@ declare global { "bq-switch": HTMLBqSwitchElement; "bq-tab": HTMLBqTabElement; "bq-tab-group": HTMLBqTabGroupElement; + "bq-toast": HTMLBqToastElement; "bq-tooltip": HTMLBqTooltipElement; } } @@ -1454,6 +1492,36 @@ declare namespace LocalJSX { */ "value"?: string; } + interface BqToast { + /** + * If true will hide toast icon + */ + "hideIcon"?: boolean; + /** + * Callback handler to be called when the notification is hidden + */ + "onBqHide"?: (event: BqToastCustomEvent) => void; + /** + * Callback handler to be called when the notification is shown + */ + "onBqShow"?: (event: BqToastCustomEvent) => void; + /** + * If true, the toast will be shown + */ + "open"?: boolean; + /** + * Placement of toast + */ + "placement"?: TToastPlacement; + /** + * The length of time, in milliseconds, after which the toast will close itself + */ + "time"?: number; + /** + * Type of toast + */ + "type"?: TToastType; + } interface BqTooltip { /** * Set the action when the tooltip should be displayed, on hover (default) or click @@ -1496,6 +1564,7 @@ declare namespace LocalJSX { "bq-switch": BqSwitch; "bq-tab": BqTab; "bq-tab-group": BqTabGroup; + "bq-toast": BqToast; "bq-tooltip": BqTooltip; } } @@ -1537,6 +1606,7 @@ declare module "@stencil/core" { "bq-switch": LocalJSX.BqSwitch & JSXBase.HTMLAttributes; "bq-tab": LocalJSX.BqTab & JSXBase.HTMLAttributes; "bq-tab-group": LocalJSX.BqTabGroup & JSXBase.HTMLAttributes; + "bq-toast": LocalJSX.BqToast & JSXBase.HTMLAttributes; "bq-tooltip": LocalJSX.BqTooltip & JSXBase.HTMLAttributes; } } diff --git a/packages/bee-q/src/components/icon/readme.md b/packages/bee-q/src/components/icon/readme.md index d4288d003..41cedf6a2 100644 --- a/packages/bee-q/src/components/icon/readme.md +++ b/packages/bee-q/src/components/icon/readme.md @@ -40,6 +40,7 @@ Icons are simplified images that graphically explain the meaning of an object on - [bq-dialog](../dialog) - [bq-notification](../notification) - [bq-switch](../switch) + - [bq-toast](../toast) ### Graph ```mermaid @@ -48,6 +49,7 @@ graph TD; bq-dialog --> bq-icon bq-notification --> bq-icon bq-switch --> bq-icon + bq-toast --> bq-icon style bq-icon fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/packages/bee-q/src/components/toast/__tests__/bq-toast.e2e.ts b/packages/bee-q/src/components/toast/__tests__/bq-toast.e2e.ts new file mode 100644 index 000000000..20fb1d0fb --- /dev/null +++ b/packages/bee-q/src/components/toast/__tests__/bq-toast.e2e.ts @@ -0,0 +1,100 @@ +import { newE2EPage } from '@stencil/core/testing'; + +import { computedStyle } from '../../../shared/test-utils/computedStyle'; + +describe('bq-toast', () => { + it('should render', async () => { + const page = await newE2EPage(); + await page.setContent(''); + + const element = await page.find('bq-toast'); + + expect(element).toHaveClass('hydrated'); + }); + + it('should have shadow root', async () => { + const page = await newE2EPage(); + await page.setContent(''); + + const element = await page.find('bq-toast'); + + expect(element.shadowRoot).not.toBeNull(); + }); + + it('should display text', async () => { + const page = await newE2EPage(); + await page.setContent('Text'); + + const element = await page.find('bq-toast'); + + expect(element).toEqualText('Text'); + }); + + it('should display info icon by default', async () => { + const page = await newE2EPage(); + await page.setContent('Text'); + + const iconWrapper = await page.find('bq-toast >>> bq-icon'); + + expect(iconWrapper).toEqualAttribute('name', 'info'); + }); + + it('should display success icon', async () => { + const page = await newE2EPage(); + await page.setContent('Text'); + + const iconWrapper = await page.find('bq-toast >>> bq-icon'); + + expect(iconWrapper).toEqualAttribute('name', 'check-circle'); + }); + + it('should display custom icon', async () => { + const page = await newE2EPage(); + await page.setContent(` + + Text + + + `); + + const iconWrapperName = await page.$eval('bq-toast', (element) => { + const slotElement = element.shadowRoot.querySelector('slot[name="icon"]'); + + const assignedElements = slotElement.assignedElements({ flatten: true })[0]; + + return assignedElements.getAttribute('name'); + }); + + expect(iconWrapperName).toEqualText('star'); + }); + + it('should respect design style', async () => { + const page = await newE2EPage(); + await page.setContent(`Text`); + + const styleProps = ['padding', 'borderRadius', 'gap'] as const; + + const style = await computedStyle(page, 'bq-toast >>> [part="wrapper"]', styleProps); + + expect(style).toEqual({ padding: '12px 16px', borderRadius: '8px', gap: '8px' }); + }); + + it('should call methods', async () => { + const page = await newE2EPage(); + await page.setContent('Test'); + + const element = await page.find('bq-toast'); + + await element.callMethod('show'); + await page.waitForChanges(); + + expect(element).toEqualAttribute('aria-hidden', 'false'); + expect(element).toEqualAttribute('hidden', 'false'); + + await element.callMethod('hide'); + await page.waitForChanges(); + + expect(element).toEqualAttribute('aria-hidden', 'true'); + expect(element).toEqualAttribute('hidden', 'true'); + }); +}); diff --git a/packages/bee-q/src/components/toast/_storybook/bq-toast.mdx b/packages/bee-q/src/components/toast/_storybook/bq-toast.mdx new file mode 100644 index 000000000..6145319b9 --- /dev/null +++ b/packages/bee-q/src/components/toast/_storybook/bq-toast.mdx @@ -0,0 +1,19 @@ +import { ArgTypes, Title, Subtitle } from '@storybook/addon-docs'; + +Toast + +The Toast Component is a UI element used to provide short, non-interruptive notifications to users. + +Usage + +Toasts are typically displayed briefly at the bottom of the screen and disappear after a short amount of time. Toasts are commonly used to provide confirmation messages, error messages, or progress updates. + +👍 When to use + +- Providing confirmation messages for actions, such as confirming the successful submission of a form or successful completion of a task. +- Displaying error messages for issues or problems, such as indicating a required field is missing or a network error has occurred. +- Presenting progress updates for long-running actions, such as indicating the status of a download or the progress of a task + +Properties + + diff --git a/packages/bee-q/src/components/toast/_storybook/bq-toast.stories.tsx b/packages/bee-q/src/components/toast/_storybook/bq-toast.stories.tsx new file mode 100644 index 000000000..ea0a5c235 --- /dev/null +++ b/packages/bee-q/src/components/toast/_storybook/bq-toast.stories.tsx @@ -0,0 +1,138 @@ +import type { Args, Meta, StoryObj } from '@storybook/web-components'; +import { html } from 'lit-html'; + +import mdx from './bq-toast.mdx'; +import { getRandomFromArray } from '../../../shared/utils'; +import { TOAST_PLACEMENT, TOAST_TYPE } from '../bq-toast.types'; + +const meta: Meta = { + title: 'Components/Toast', + component: 'bq-toast', + parameters: { + docs: { + page: mdx, + }, + }, + argTypes: { + type: { control: 'select', options: [...TOAST_TYPE] }, + placement: { control: 'select', options: [...TOAST_PLACEMENT] }, + 'hide-icon': { control: 'boolean' }, + open: { control: 'boolean' }, + time: { control: 'number' }, + text: { control: 'text', table: { disable: true } }, + // Event handlers + bqShow: { action: 'bqShow' }, + bqHide: { action: 'bqHide' }, + }, + args: { + type: 'info', + placement: 'bottom-center', + 'hide-icon': false, + open: false, + time: 3000, + text: 'This is a message', + }, +}; +export default meta; + +type Story = StoryObj; + +const Template = (args: Args) => { + const onToastHide = (event) => { + args.bqHide(event); + event.preventDefault(); + }; + + return html`${TOAST_TYPE.map( + (type) => html` +
+ + ${args.text} + ${type === 'custom' ? html`` : null} + +
+ `, + )} `; +}; + +export const Default: Story = { + render: Template, + argTypes: { + type: { control: 'select', table: { disable: true } }, + time: { control: 'number', table: { disable: true } }, + open: { control: 'boolean', table: { disable: true } }, + }, + args: { + open: true, + }, +}; + +const CustomIconTemplate = (args: Args) => { + const onToastHide = (event) => { + args.bqHide(event); + event.preventDefault(); + }; + + return html` + + ${args.text} + + + `; +}; + +export const Custom: Story = { + render: CustomIconTemplate, + args: { + type: 'success', + open: true, + }, +}; + +const StackableTemplate = (args: Args) => { + const toggleToast = async () => { + const toast = document.createElement('bq-toast'); + + const [type] = getRandomFromArray(TOAST_TYPE as unknown as unknown[], 1); + + Object.assign(toast, { + type: type, + hideIcon: args['hide-icon'], + time: args.time, + open: args.open, + placement: args.placement, + innerHTML: args.text, + }); + + document.body.append(toast); + + await toast.toast(); + }; + + return html` Toggle toast `; +}; + +export const Stackable: Story = { + render: StackableTemplate, + argTypes: { + type: { control: 'select', table: { disable: true } }, + open: { control: 'boolean', table: { disable: true } }, + }, + args: {}, +}; diff --git a/packages/bee-q/src/components/toast/bq-toast.tsx b/packages/bee-q/src/components/toast/bq-toast.tsx new file mode 100644 index 000000000..67e451ba5 --- /dev/null +++ b/packages/bee-q/src/components/toast/bq-toast.tsx @@ -0,0 +1,225 @@ +import { h, Host, Element, Event, EventEmitter, Component, Prop, Watch, Method, Listen } from '@stencil/core'; + +import { TOAST_PLACEMENT, TOAST_TYPE, TToastPlacement, TToastType } from './bq-toast.types'; +import { TDebounce, debounce, validatePropValue } from '../../shared/utils'; + +const toastPortal = Object.assign(document.createElement('div'), { className: 'bq-toast-portal' }); + +/** + * @part wrapper - The component's internal wrapper inside the shadow DOM. + * @part icon-info - The `
` container that holds the icon component. + * @part base - The `
` container of the internal bq-icon component. + * @part svg - The `` element of the internal bq-icon component. + */ +@Component({ + tag: 'bq-toast', + styleUrl: './scss/bq-toast.scss', + shadow: true, +}) +export class BqToast { + // Own Properties + // ==================== + + private autoDismissDebounce: TDebounce; + + // Reference to host HTML element + // =================================== + + @Element() el!: HTMLBqToastElement; + + // State() variables + // Inlined decorator, alphabetical order + // ======================================= + + // Public Property API + // ======================== + + /** Type of toast */ + @Prop({ reflect: true, mutable: true }) type: TToastType = 'info'; + + /** Placement of toast */ + @Prop({ reflect: true, mutable: true }) placement: TToastPlacement = 'bottom-center'; + + /** If true will hide toast icon */ + @Prop({ reflect: true, mutable: true }) hideIcon = false; + + /** If true, the toast will be shown */ + @Prop({ reflect: true, mutable: true }) open: boolean; + + /** The length of time, in milliseconds, after which the toast will close itself */ + @Prop({ reflect: true }) time: number = 3000; + + // Prop lifecycle events + // ======================= + + @Watch('type') + @Watch('placement') + checkPropValues() { + validatePropValue(TOAST_TYPE, 'default', this.el, 'type'); + validatePropValue(TOAST_PLACEMENT, 'bottom-center', this.el, 'placement'); + + toastPortal.classList.remove(...TOAST_PLACEMENT); + toastPortal.classList.add(this.placement); + } + + @Watch('time') + handleTimeChange() { + this.autoDismissDebounce?.cancel(); + + this.time = Math.max(0, this.time); + + this.autoDismissDebounce = debounce(() => { + this.hide(); + }, this.time); + } + + @Watch('open') + handleOpenChange() { + this.autoDismissDebounce?.cancel(); + + if (this.open) { + this.autoDismissDebounce?.(); + } + } + + // Events section + // Requires JSDocs for public API documentation + // ============================================== + + /** Callback handler to be called when the notification is hidden */ + @Event() bqHide: EventEmitter; + + /** Callback handler to be called when the notification is shown */ + @Event() bqShow: EventEmitter; + + // Component lifecycle events + // Ordered by their natural call order + // ===================================== + + componentWillLoad() { + this.checkPropValues(); + this.handleTimeChange(); + this.handleOpenChange(); + } + + disconnectedCallback() { + this.autoDismissDebounce?.cancel(); + } + + // Listeners + // ============== + + @Listen('bqHide') + onNotificationHide() { + try { + toastPortal.removeChild(this.el); + // Remove the toast portal from the DOM when there are no more toasts + if (toastPortal.querySelector('bq-toast') === null) { + toastPortal.remove(); + } + } catch (error) { + /** + * Skip DOMException error since it could be possible that + * in some situations the notification portal is missing + */ + if (error instanceof DOMException) return; + throw error; + } + } + + // Public methods API + // These methods are exposed on the host element. + // Always use two lines. + // Public Methods must be async. + // Requires JSDocs for public API documentation. + // =============================================== + + @Method() + async show(): Promise { + this.handleShow(); + } + + @Method() + async hide(): Promise { + this.handleHide(); + } + + @Method() + async toast() { + if (toastPortal.parentElement === null) { + document.body.append(toastPortal); + } + + toastPortal.appendChild(this.el); + + requestAnimationFrame(() => { + this.show(); + }); + } + + // Local methods + // Internal business logic. + // These methods cannot be called from the host element. + // ======================================================= + + private handleShow = () => { + const ev = this.bqShow.emit(this.el); + if (!ev.defaultPrevented) { + this.open = true; + } + }; + + private handleHide = () => { + const ev = this.bqHide.emit(this.el); + if (!ev.defaultPrevented) { + this.open = false; + } + }; + + private get iconName() { + switch (this.type) { + case 'success': { + return 'check-circle'; + } + case 'error': { + return 'x-circle'; + } + case 'loading': { + return 'spinner-gap'; + } + case 'alert': { + return 'warning'; + } + case 'info': { + return 'info'; + } + default: { + return 'info'; + } + } + } + + // render() function + // Always the last one in the class. + // =================================== + + render() { + return ( +
+ + + +
+ + + + ); + } +} diff --git a/packages/bee-q/src/components/toast/bq-toast.types.ts b/packages/bee-q/src/components/toast/bq-toast.types.ts new file mode 100644 index 000000000..81854f420 --- /dev/null +++ b/packages/bee-q/src/components/toast/bq-toast.types.ts @@ -0,0 +1,12 @@ +export const TOAST_TYPE = ['info', 'success', 'alert', 'error', 'loading', 'custom'] as const; +export type TToastType = (typeof TOAST_TYPE)[number]; + +export const TOAST_PLACEMENT = [ + 'top-center', + 'top-right', + 'bottom-right', + 'bottom-center', + 'bottom-left', + 'top-left', +] as const; +export type TToastPlacement = (typeof TOAST_PLACEMENT)[number]; diff --git a/packages/bee-q/src/components/toast/readme.md b/packages/bee-q/src/components/toast/readme.md new file mode 100644 index 000000000..cf78b5b44 --- /dev/null +++ b/packages/bee-q/src/components/toast/readme.md @@ -0,0 +1,86 @@ +# bq-toast + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ----------- | ----------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------- | +| `hideIcon` | `hide-icon` | If true will hide toast icon | `boolean` | `false` | +| `open` | `open` | If true, the toast will be shown | `boolean` | `undefined` | +| `placement` | `placement` | Placement of toast | `"bottom-center" \| "bottom-left" \| "bottom-right" \| "top-center" \| "top-left" \| "top-right"` | `'bottom-center'` | +| `time` | `time` | The length of time, in milliseconds, after which the toast will close itself | `number` | `3000` | +| `type` | `type` | Type of toast | `"alert" \| "custom" \| "error" \| "info" \| "loading" \| "success"` | `'info'` | + + +## Events + +| Event | Description | Type | +| -------- | ------------------------------------------------------------- | --------------------------------- | +| `bqHide` | Callback handler to be called when the notification is hidden | `CustomEvent` | +| `bqShow` | Callback handler to be called when the notification is shown | `CustomEvent` | + + +## Methods + +### `hide() => Promise` + + + +#### Returns + +Type: `Promise` + + + +### `show() => Promise` + + + +#### Returns + +Type: `Promise` + + + +### `toast() => Promise` + + + +#### Returns + +Type: `Promise` + + + + +## Shadow Parts + +| Part | Description | +| ------------- | -------------------------------------------------------- | +| `"base"` | The `
` container of the internal bq-icon component. | +| `"icon"` | | +| `"icon-info"` | The `
` container that holds the icon component. | +| `"svg"` | The `` element of the internal bq-icon component. | +| `"wrapper"` | The component's internal wrapper inside the shadow DOM. | + + +## Dependencies + +### Depends on + +- [bq-icon](../icon) + +### Graph +```mermaid +graph TD; + bq-toast --> bq-icon + style bq-toast fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/packages/bee-q/src/components/toast/scss/bq-toast.scss b/packages/bee-q/src/components/toast/scss/bq-toast.scss new file mode 100644 index 000000000..57868e88e --- /dev/null +++ b/packages/bee-q/src/components/toast/scss/bq-toast.scss @@ -0,0 +1,44 @@ +/* -------------------------------------------------------------------------- */ +/* Toast styles */ +/* -------------------------------------------------------------------------- */ + +@import './bq-toast.variables'; + +:host { + @apply inline-block animate-slide-in will-change-transform; +} + +.bq-toast { + @apply flex items-center gap-[var(--bq-toast--gap)] px-[var(--bq-toast--padding-x)] py-[var(--bq-toast--padding-y)]; + @apply rounded-[var(--bq-toast--border-radius)] bg-[var(--bq-toast--background)] shadow-[shadow:var(--bq-toast--box-shadow)]; + + border: var(--bq-toast--border-width) var(--bq-toast--border-style) var(--bq-toast--border-color); +} + +.bq-toast--icon { + @apply flex; + + &.info { + @apply text-[var(--bq-toast--icon-color-info)]; + } + + &.success { + @apply text-[var(--bq-toast--icon-color-success)]; + } + + &.alert { + @apply text-[var(--bq-toast--icon-color-alert)]; + } + + &.error { + @apply text-[var(--bq-toast--icon-color-error)]; + } + + &.loading { + @apply animate-spin text-[var(--bq-toast--icon-color-loading)]; + } + + &.custom { + @apply text-[var(--bq-toast--icon-color-custom)]; + } +} diff --git a/packages/bee-q/src/components/toast/scss/bq-toast.variables.scss b/packages/bee-q/src/components/toast/scss/bq-toast.variables.scss new file mode 100644 index 000000000..4e1b564da --- /dev/null +++ b/packages/bee-q/src/components/toast/scss/bq-toast.variables.scss @@ -0,0 +1,42 @@ +/* -------------------------------------------------------------------------- */ +/* Toast custom properties */ +/* -------------------------------------------------------------------------- */ + +:host { + /** + * @prop --bq-toast--background: Toast background color + * @prop --bq-toast--box-shadow: Toast box shadow + * @prop --bq-toast--padding-y: Toast vertical padding + * @prop --bq-toast--padding-x: Toast horizontal padding + * @prop --bq-toast--gap: Toast distance between icon and text + * @prop --bq-toast--border-radius: Toast border radius + * @prop --bq-toast--border-color: Toast border color + * @prop --bq-toast--border-style: Toast border style + * @prop --bq-toast--border-width: Toast border width + * @prop --bq-toast--icon-color-info: Toast icon color when type is 'info' + * @prop --bq-toast--icon-color-success: Toast icon color when type is 'success' + * @prop --bq-toast--icon-color-alert: Toast icon color when type is 'alert' + * @prop --bq-toast--icon-color-error: Toast icon color when type is 'error' + * @prop --bq-toast--icon-color-loading: Toast icon color when type is 'loading' + * @prop --bq-toast--icon-color-custom: Toast icon color when type is 'custom' + */ + + --bq-toast--background: theme('colors.bg.primary'); + --bq-toast--box-shadow: theme('boxShadow.l'); + + --bq-toast--padding-y: theme('spacing.s'); + --bq-toast--padding-x: theme('spacing.m'); + --bq-toast--gap: theme('spacing.xs'); + + --bq-toast--border-radius: theme('borderRadius.s'); + --bq-toast--border-width: unset; + --bq-toast--border-style: none; + --bq-toast--border-color: theme('colors.transparent'); + + --bq-toast--icon-color-info: theme('colors.icon.brand'); + --bq-toast--icon-color-success: theme('colors.icon.success'); + --bq-toast--icon-color-alert: theme('colors.icon.warning'); + --bq-toast--icon-color-error: theme('colors.icon.danger'); + --bq-toast--icon-color-loading: theme('colors.icon.brand'); + --bq-toast--icon-color-custom: theme('colors.icon.primary'); +} diff --git a/packages/bee-q/src/global/styles/utils/_utility.scss b/packages/bee-q/src/global/styles/utils/_utility.scss index 61e8eb9d5..42a124b71 100644 --- a/packages/bee-q/src/global/styles/utils/_utility.scss +++ b/packages/bee-q/src/global/styles/utils/_utility.scss @@ -37,3 +37,37 @@ .bq-notification-portal bq-notification { @apply m-4; } + +/** + * Use these classes for the toast portal container + */ + +.bq-toast-portal { + @apply fixed z-50 flex max-h-full flex-col gap-s; + // NOTE: on mobile we display toast full screen top / bottom + @apply left-0 w-full px-m sm:left-auto sm:w-fit sm:px-0; + + &.top-left { + @apply top-xxl2 sm:left-xxl2; + } + + &.top-center { + @apply top-xxl2 sm:left-1/2 sm:-translate-x-1/2; + } + + &.top-right { + @apply top-xxl2 sm:right-xxl2; + } + + &.bottom-left { + @apply bottom-xxl2 sm:left-xxl2; + } + + &.bottom-center { + @apply bottom-xxl2 sm:left-1/2 sm:-translate-x-1/2; + } + + &.bottom-right { + @apply bottom-xxl2 sm:right-xxl2; + } +} diff --git a/tailwind.config.ts b/tailwind.config.ts index 7fa8c0cc5..87c90f0ae 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -85,6 +85,20 @@ const config: Config = { xxl3: 'var(--bq-spacing-xxl3)', xxl4: 'var(--bq-spacing-xxl4)', }, + keyframes: { + 'fade-in': { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + 'slide-up': { + '0%': { + transform: 'translateY(10px)', + }, + }, + }, + animation: { + 'slide-in': 'fade-in 0.3s ease, slide-up 0.3s ease', + }, }, }, plugins: [