Loading src/Playground.vue +4 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import Rating from '@components/Rating/Rating.vue'; import HomeIcon from '@icons/Mono/HomeIcon.vue'; import ProgressBar from '@components/ProgressBar/ProgressBar.vue'; import Carousel from '@components/Carousel/Carousel.vue'; import Toast from '@components/Toast/Toast.vue'; const visibleDrawer = ref(false); const sliderOptions: ISliderOptions[] = [ Loading Loading @@ -174,11 +175,14 @@ const selectOptions = [ ]; const knob = ref(0); const pbValue = ref(0); const toast = ref(false); const openDrawer = () => (visibleDrawer.value = true); </script> <template> <h2 class="title gradient-text">Playground</h2> <Button label="Open toast" @click="toast = true" /> <Toast v-model="toast" type="info" position="bottomRight" width="500px" /> <Rating theme="red"> <template #offIcon> <CrossIcon color="red" /> Loading src/common/helpers/common.ts +1 −1 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ export const convertThemeToSecondaryColor = (theme: TThemeColor, darkness: TDark : convertThemeToColor(theme, String(100 + ((+darkness + 600) % 900))); }; export const getValueFromSize = (size: TSize, options: string[] | number[]) => { export const getValueFromSize = <T>(size: TSize, options: T[]): T => { if (size === 'normal') return options[1]; if (size === 'large') return options[2]; if (size === 'huge') return options[3]; Loading src/common/interfaces/componentsProps.ts +3 −1 Original line number Diff line number Diff line Loading @@ -290,14 +290,16 @@ export interface ITSProps { } export interface IToastProps { active?: boolean; duration?: number | false; type?: TToastType; theme?: TThemeColor; size?: TSize; text?: string; header?: string; icon?: TIcon; width?: string; position?: TExpandedPosition; width?: string; static?: boolean; } Loading src/components/Toast/Toast.stories.ts +25 −0 Original line number Diff line number Diff line import type { Meta, StoryObj } from '@storybook/vue3'; import Toast from './Toast.vue'; import Button from '@components/Button/Button.vue'; import { iconsSet } from '@/common/constants/icons'; const meta: Meta = { Loading @@ -17,6 +18,12 @@ const meta: Meta = { argTypes: { type: { control: 'select', options: ['success', 'info', 'warn', 'error'] }, size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, position: { control: 'select', options: ['topRight', 'bottomRight', 'bottomLeft', 'topLeft', 'top', 'right', 'bottom', 'left'], }, active: { control: 'boolean' }, static: { control: 'boolean' }, width: { control: 'text' }, header: { control: 'text' }, text: { control: 'text' }, Loading Loading @@ -53,12 +60,25 @@ export const Simple: Story = { args: { active: true, }, render: (args) => ({ components: { Toast, Button }, data() { return { active: false, }; }, setup() { return { args }; }, template: '<Toast v-model="active" v-bind="args" /><Button @click="active = true" label="Open toast"/>', }), }; export const Info: Story = { args: { active: true, type: 'info', static: true, }, }; Loading @@ -66,6 +86,7 @@ export const Warn: Story = { args: { active: true, type: 'warn', static: true, }, }; Loading @@ -73,6 +94,7 @@ export const Error: Story = { args: { active: true, type: 'error', static: true, }, }; Loading @@ -80,6 +102,7 @@ export const Small: Story = { args: { active: true, size: 'small', static: true, }, }; Loading @@ -91,6 +114,7 @@ export const Large: Story = { text: 'This is a text of large toast!', icon: 'Award', theme: 'sky', static: true, }, }; Loading @@ -103,5 +127,6 @@ export const Huge: Story = { icon: 'Badge', theme: 'purple', header: 'Custom header', static: true, }, }; src/components/Toast/Toast.vue +40 −16 Original line number Diff line number Diff line <script setup lang="ts"> import type { IToastProps } from '@interfaces/componentsProps'; import { computed, ref } from 'vue'; import { computed, watch } from 'vue'; import { convertThemeToColor, getValueFromSize } from '@helpers/common'; import type { TToastType } from '@interfaces/componentsProp'; import { iconsSet } from '@/common/constants/icons'; Loading @@ -9,13 +9,13 @@ import type { TThemeColor } from '@interfaces/common'; const props = withDefaults(defineProps<IToastProps>(), { type: 'success', text: 'This is a toast about success.', size: 'normal', width: '300px', position: 'topRight', duration: 5, }); const typeToTheme: Record<TToastType, string> = { const typeToTheme: Record<TToastType, TThemeColor> = { success: 'green', info: 'blue', warn: 'yellow', Loading @@ -36,7 +36,7 @@ const typeToIcon: Record<TToastType, string> = { error: 'CrossRound', }; const active = ref<boolean>(false); const active = defineModel<boolean>(); const themeColor = computed<TThemeColor>(() => props.theme ?? typeToTheme[props.type]); const header = computed<string>(() => props.header ?? typeToHeader[props.type]); Loading @@ -52,7 +52,9 @@ const backgroundColor = computed(() => ); const borderColor = computed(() => convertThemeToColor(themeColor.value, '500')); const fontSize = computed(() => getValueFromSize(props.size, ['12px', '16px', '20px', '24px'])); const padding = computed(() => getValueFromSize(props.size, ['7px 10px', '10px 15px', '14px 20px', '20px 30px'])); const padding = computed( () => getValueFromSize(props.size, ['7px 10px', '10px 15px', '14px 20px', '20px 30px']) as string, ); const gap = computed(() => props.size === 'normal' ? '10px' : props.size === 'large' || props.size === 'huge' ? '15px' : '5px', ); Loading @@ -62,38 +64,58 @@ const textMargin = computed(() => { }); const positionParts = computed(() => { const result = []; if (props.position.length < 7) return [props.position]; const position = props.position.toLowerCase(); if (position.includes('top')) result.push('top'); if (position.includes('bottom')) result.push('bottom'); if (position.includes('left')) result.push('left'); if (position.includes('right')) result.push('right'); return result; }); const styles = computed(() => { if (props.static) return ''; if (positionParts.value.length === 1) { const position = positionParts.value[0]; if (position === 'left' || position === 'right') return `${position}: -100%; top: 50%; transform: translateY(-50%);`; return `${position}: -100%; left: 50%; transform: translateX(-50%);`; } return `${positionParts.value[0]}: -100%; ${positionParts.value[1]}: 20px`; }); const activeStyles = computed(() => { let result = ''; if (positionParts.value[0] === 'top') result += 'top: 0;'; if (positionParts.value[0] === 'bottom') result += 'bottom: 0;'; if (positionParts.value[1] === 'left') result += 'left: 0;'; if (positionParts.value[1] === 'right') result += 'right: 0;'; return result; if (positionParts.value.length === 1) return `${positionParts.value[0]}: 20px`; return `${positionParts.value[0]}: 20px; ${positionParts.value[1]}: 20px`; }); const closeToast = () => {}; const closeToast = () => (active.value = false); let timeout: number; if (props.duration) { watch(active, () => { if (active.value) { timeout = setTimeout(() => (active.value = false), (props.duration as number) * 1000); } else { clearTimeout(timeout); } }); } </script> <template> <section class="toast-container" :style="`position: ${static ? 'relative' : 'absolute'}; ${positionParts[0]}: ${positionParts[0] === 'top' ? '-100%' : '100%'}; ${positionParts[1]}: ${positionParts[1] === 'left' ? '-100%' : '100%'}; :style="`position: ${static ? 'relative' : 'fixed'}; ${styles}; ${active ? activeStyles : null}`" > <h3 class="toast-header" :style="`font-size: calc(${fontSize} + 4px)`"> <component :is="iconsSet[icon]" :color="color" :size="iconSize" /> <span class="toast-header-text">{{ header }}</span> </h3> <p class="toast-text">{{ text }}</p> <p class="toast-text">{{ text ?? `This is a toast about ${type}` }}</p> <CrossIcon @click="closeToast" class="toast-close_button" Loading @@ -106,10 +128,12 @@ const closeToast = () => {}; <style scoped> .toast-container { z-index: 9999; padding: v-bind(padding); border: 1px solid v-bind(borderColor); border-radius: 5px; width: v-bind(width); transition: all 0.4s ease-in-out; ::before { content: ''; position: absolute; Loading Loading
src/Playground.vue +4 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import Rating from '@components/Rating/Rating.vue'; import HomeIcon from '@icons/Mono/HomeIcon.vue'; import ProgressBar from '@components/ProgressBar/ProgressBar.vue'; import Carousel from '@components/Carousel/Carousel.vue'; import Toast from '@components/Toast/Toast.vue'; const visibleDrawer = ref(false); const sliderOptions: ISliderOptions[] = [ Loading Loading @@ -174,11 +175,14 @@ const selectOptions = [ ]; const knob = ref(0); const pbValue = ref(0); const toast = ref(false); const openDrawer = () => (visibleDrawer.value = true); </script> <template> <h2 class="title gradient-text">Playground</h2> <Button label="Open toast" @click="toast = true" /> <Toast v-model="toast" type="info" position="bottomRight" width="500px" /> <Rating theme="red"> <template #offIcon> <CrossIcon color="red" /> Loading
src/common/helpers/common.ts +1 −1 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ export const convertThemeToSecondaryColor = (theme: TThemeColor, darkness: TDark : convertThemeToColor(theme, String(100 + ((+darkness + 600) % 900))); }; export const getValueFromSize = (size: TSize, options: string[] | number[]) => { export const getValueFromSize = <T>(size: TSize, options: T[]): T => { if (size === 'normal') return options[1]; if (size === 'large') return options[2]; if (size === 'huge') return options[3]; Loading
src/common/interfaces/componentsProps.ts +3 −1 Original line number Diff line number Diff line Loading @@ -290,14 +290,16 @@ export interface ITSProps { } export interface IToastProps { active?: boolean; duration?: number | false; type?: TToastType; theme?: TThemeColor; size?: TSize; text?: string; header?: string; icon?: TIcon; width?: string; position?: TExpandedPosition; width?: string; static?: boolean; } Loading
src/components/Toast/Toast.stories.ts +25 −0 Original line number Diff line number Diff line import type { Meta, StoryObj } from '@storybook/vue3'; import Toast from './Toast.vue'; import Button from '@components/Button/Button.vue'; import { iconsSet } from '@/common/constants/icons'; const meta: Meta = { Loading @@ -17,6 +18,12 @@ const meta: Meta = { argTypes: { type: { control: 'select', options: ['success', 'info', 'warn', 'error'] }, size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, position: { control: 'select', options: ['topRight', 'bottomRight', 'bottomLeft', 'topLeft', 'top', 'right', 'bottom', 'left'], }, active: { control: 'boolean' }, static: { control: 'boolean' }, width: { control: 'text' }, header: { control: 'text' }, text: { control: 'text' }, Loading Loading @@ -53,12 +60,25 @@ export const Simple: Story = { args: { active: true, }, render: (args) => ({ components: { Toast, Button }, data() { return { active: false, }; }, setup() { return { args }; }, template: '<Toast v-model="active" v-bind="args" /><Button @click="active = true" label="Open toast"/>', }), }; export const Info: Story = { args: { active: true, type: 'info', static: true, }, }; Loading @@ -66,6 +86,7 @@ export const Warn: Story = { args: { active: true, type: 'warn', static: true, }, }; Loading @@ -73,6 +94,7 @@ export const Error: Story = { args: { active: true, type: 'error', static: true, }, }; Loading @@ -80,6 +102,7 @@ export const Small: Story = { args: { active: true, size: 'small', static: true, }, }; Loading @@ -91,6 +114,7 @@ export const Large: Story = { text: 'This is a text of large toast!', icon: 'Award', theme: 'sky', static: true, }, }; Loading @@ -103,5 +127,6 @@ export const Huge: Story = { icon: 'Badge', theme: 'purple', header: 'Custom header', static: true, }, };
src/components/Toast/Toast.vue +40 −16 Original line number Diff line number Diff line <script setup lang="ts"> import type { IToastProps } from '@interfaces/componentsProps'; import { computed, ref } from 'vue'; import { computed, watch } from 'vue'; import { convertThemeToColor, getValueFromSize } from '@helpers/common'; import type { TToastType } from '@interfaces/componentsProp'; import { iconsSet } from '@/common/constants/icons'; Loading @@ -9,13 +9,13 @@ import type { TThemeColor } from '@interfaces/common'; const props = withDefaults(defineProps<IToastProps>(), { type: 'success', text: 'This is a toast about success.', size: 'normal', width: '300px', position: 'topRight', duration: 5, }); const typeToTheme: Record<TToastType, string> = { const typeToTheme: Record<TToastType, TThemeColor> = { success: 'green', info: 'blue', warn: 'yellow', Loading @@ -36,7 +36,7 @@ const typeToIcon: Record<TToastType, string> = { error: 'CrossRound', }; const active = ref<boolean>(false); const active = defineModel<boolean>(); const themeColor = computed<TThemeColor>(() => props.theme ?? typeToTheme[props.type]); const header = computed<string>(() => props.header ?? typeToHeader[props.type]); Loading @@ -52,7 +52,9 @@ const backgroundColor = computed(() => ); const borderColor = computed(() => convertThemeToColor(themeColor.value, '500')); const fontSize = computed(() => getValueFromSize(props.size, ['12px', '16px', '20px', '24px'])); const padding = computed(() => getValueFromSize(props.size, ['7px 10px', '10px 15px', '14px 20px', '20px 30px'])); const padding = computed( () => getValueFromSize(props.size, ['7px 10px', '10px 15px', '14px 20px', '20px 30px']) as string, ); const gap = computed(() => props.size === 'normal' ? '10px' : props.size === 'large' || props.size === 'huge' ? '15px' : '5px', ); Loading @@ -62,38 +64,58 @@ const textMargin = computed(() => { }); const positionParts = computed(() => { const result = []; if (props.position.length < 7) return [props.position]; const position = props.position.toLowerCase(); if (position.includes('top')) result.push('top'); if (position.includes('bottom')) result.push('bottom'); if (position.includes('left')) result.push('left'); if (position.includes('right')) result.push('right'); return result; }); const styles = computed(() => { if (props.static) return ''; if (positionParts.value.length === 1) { const position = positionParts.value[0]; if (position === 'left' || position === 'right') return `${position}: -100%; top: 50%; transform: translateY(-50%);`; return `${position}: -100%; left: 50%; transform: translateX(-50%);`; } return `${positionParts.value[0]}: -100%; ${positionParts.value[1]}: 20px`; }); const activeStyles = computed(() => { let result = ''; if (positionParts.value[0] === 'top') result += 'top: 0;'; if (positionParts.value[0] === 'bottom') result += 'bottom: 0;'; if (positionParts.value[1] === 'left') result += 'left: 0;'; if (positionParts.value[1] === 'right') result += 'right: 0;'; return result; if (positionParts.value.length === 1) return `${positionParts.value[0]}: 20px`; return `${positionParts.value[0]}: 20px; ${positionParts.value[1]}: 20px`; }); const closeToast = () => {}; const closeToast = () => (active.value = false); let timeout: number; if (props.duration) { watch(active, () => { if (active.value) { timeout = setTimeout(() => (active.value = false), (props.duration as number) * 1000); } else { clearTimeout(timeout); } }); } </script> <template> <section class="toast-container" :style="`position: ${static ? 'relative' : 'absolute'}; ${positionParts[0]}: ${positionParts[0] === 'top' ? '-100%' : '100%'}; ${positionParts[1]}: ${positionParts[1] === 'left' ? '-100%' : '100%'}; :style="`position: ${static ? 'relative' : 'fixed'}; ${styles}; ${active ? activeStyles : null}`" > <h3 class="toast-header" :style="`font-size: calc(${fontSize} + 4px)`"> <component :is="iconsSet[icon]" :color="color" :size="iconSize" /> <span class="toast-header-text">{{ header }}</span> </h3> <p class="toast-text">{{ text }}</p> <p class="toast-text">{{ text ?? `This is a toast about ${type}` }}</p> <CrossIcon @click="closeToast" class="toast-close_button" Loading @@ -106,10 +128,12 @@ const closeToast = () => {}; <style scoped> .toast-container { z-index: 9999; padding: v-bind(padding); border: 1px solid v-bind(borderColor); border-radius: 5px; width: v-bind(width); transition: all 0.4s ease-in-out; ::before { content: ''; position: absolute; Loading