diff --git a/.storybook/preview.js b/.storybook/preview.js index a5ce23df..0bfa5e10 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -5,6 +5,7 @@ import PrimeVue from 'primevue/config'; import { getPrimeVueConfig } from '@/plugins/prime'; import Tooltip from 'primevue/tooltip'; import ConfirmationService from 'primevue/confirmationservice'; +import ToastService from 'primevue/toastservice'; import '../src/tailwind.css'; import './themes/base.css'; @@ -45,6 +46,7 @@ setup((app) => { registerToastification(app); app.use(PrimeVue, mergedConfig); app.use(ConfirmationService); + app.use(ToastService); app.directive('tooltip', Tooltip); }); diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..2daf9a77 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 CDEK-IT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/plugins/prime/stories/Toast/Toast.mdx b/src/plugins/prime/stories/Toast/Toast.mdx new file mode 100644 index 00000000..13fe793d --- /dev/null +++ b/src/plugins/prime/stories/Toast/Toast.mdx @@ -0,0 +1,78 @@ +import { Meta, Canvas, Controls, Title, Description } from '@storybook/addon-docs/blocks'; +import * as ToastStories from './Toast.stories'; + + + + +<Description /> + +## Варианты использования + +<br /> + +### Базовый +Базовый вариант `Toast`. Используйте таблицу ниже для настройки пропсов. Кнопки внизу позволяют показать живые тосты. +Иконки для каждого типа `Toast` уже "вшиты", но это поведение можно изменить (см. ниже). +<Canvas of={ToastStories.Default} /> + +#### Полный список аргументов и событий компонента. +<Controls of={ToastStories.Default} /> + +<hr /> + +### С кнопкой закрытия +Базовый вариант `Toast` с кнопкой закрытия. +<Canvas of={ToastStories.DefaultButton} /> + +### Кастомный контент +`Toast` с кастомным контейнером. +<Canvas of={ToastStories.WithContent} /> + +#### Аргументы и события +<Controls of={ToastStories.WithContent}/> + +<hr /> + +### С кастомным контейнером и кнопкой закрытия +`Toast` с кастомным контейнером и кнопкой закрытия. +<Canvas of={ToastStories.WithContentAndCloseButton} /> + +#### Аргументы и события +<Controls of={ToastStories.WithContentAndCloseButton} /> + +<hr /> + +### Ширина Toast +Ширина `Toast` задаётся через `props` — `width`. Доступные значения: +<details> + <summary>Примеры кода</summary> + + - `sm` (20rem) + ```html dark + <PBlockToast width="sm" /> + ``` + - `md` (25rem, по умолчанию) + ```html dark + <PBlockToast /> + ``` + - `lg` (30rem) + ```html dark + <PBlockToast width="lg" /> + ``` + - `xlg` (45rem) + ```html dark + <PBlockToast width="xlg" /> + ``` +</details> + +<Canvas of={ToastStories.Width} /> + +#### Аргументы и события +<Controls of={ToastStories.Width} /> + +### Позиция +Демонстрация всех доступных позиций `Toast`. Каждая кнопка показывает уведомление в соответствующей позиции. +<Canvas of={ToastStories.Position} /> + +#### Аргументы и события +<Controls of={ToastStories.Position} /> diff --git a/src/plugins/prime/stories/Toast/Toast.stories.js b/src/plugins/prime/stories/Toast/Toast.stories.js new file mode 100644 index 00000000..25ebc514 --- /dev/null +++ b/src/plugins/prime/stories/Toast/Toast.stories.js @@ -0,0 +1,364 @@ +import { PBlockToast } from '@/primeBlocks'; + +import { + Template, + TemplateCloseButton, + TemplateCustomContentWithCloseButton, + TemplateWithContent, + TemplateWidth, + TemplatePosition, +} from './Toast.template'; + +const meta = { + title: 'Prime/Messages/Toast', + component: PBlockToast, + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: `\`Toast\` используется для отображения всплывающих уведомлений поверх интерфейса.\n +Требует подключения \`ToastService\`.\n\`\`\`ts dark \nimport { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit';\`\`\` `, + }, + }, + designToken: { disable: false }, + designTokens: { prefix: '--p-toast' }, + }, + argTypes: { + position: { + control: 'select', + options: [ + 'top-right', + 'top-left', + 'top-center', + 'bottom-right', + 'bottom-left', + 'bottom-center', + 'center', + ], + description: 'Позиция тоста на экране.', + table: { + category: 'Props', + type: { + summary: + "'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center' | 'center'", + }, + }, + }, + group: { + control: 'text', + description: + 'Идентификатор группы тостов для адресной отправки сообщений.', + table: { + category: 'Props', + type: { summary: 'string' }, + }, + }, + life: { + control: 'number', + description: 'Время (мс) до автоматического закрытия тоста.', + table: { + category: 'Props', + type: { summary: 'number' }, + }, + }, + width: { + control: 'select', + options: ['sm', 'md', 'lg', 'xlg'], + description: 'Ширина тоста на экране.', + table: { + category: 'Props', + type: { summary: "'sm' | 'md' | 'lg' | 'xlg'" }, + }, + }, + }, + args: { + position: 'top-right', + group: 'basic', + life: 5000, + width: 'md', + }, +}; + +export default meta; + +export const Default = { + name: 'Toast', + render: Template, + args: { + group: 'basic', + position: 'top-right', + life: 5_000, + }, + parameters: { + docs: { + source: { + language: 'html', + code: ` +<script setup lang="ts"> +import { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit'; + +const toast = usePBlockToast(); + +const showToast = () => { + toast.add({ + severity: 'info', + summary: 'Заголовок сообщения', + detail: 'Дополнительная информация', + life: 5_000, // мс + }); +}; +</script> + +<template> + <div> + <PBlockToast /> + <Button label="Показать toast" @click="showToast" /> + </div> +</template> + `, + }, + }, + }, +}; + +export const DefaultButton = { + name: 'Toast', + render: TemplateCloseButton, + args: { + group: 'basic-button', + position: 'top-right', + life: 5_000, + }, + parameters: { + docs: { + source: { + language: 'html', + code: ` +<script setup lang="ts"> +import { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit'; + +const toast = usePBlockToast(); + +const showToast = () => { + toast.add({ + severity: 'info', + summary: 'Заголовок сообщения', + detail: 'Дополнительная информация', + life: 5_000, + closable: true, // по умолчанию false + }); +}; +</script> + +<template> + <div> + <PBlockToast /> + <Button label="Показать toast" @click="showToast" /> + </div> +</template> + `, + }, + }, + }, +}; + +export const WithContent = { + render: TemplateWithContent, + args: { + group: 'content', + position: 'top-right', + life: 5_000, + }, + parameters: { + docs: { + source: { + language: 'html', + code: ` +<script setup lang="ts"> +import { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit'; +import { IconCircleCheck } from '@tabler/icons-vue'; // Указано для примера. + +const toast = usePBlockToast(); + +const showToast = () => { + toast.add({ + severity: 'info', + summary: 'Заголовок сообщения', + detail: 'Дополнительная информация', + icon: 'ti-circle-info', + life: 5_000, + }); +}; +</script> + +<template> + <div> + <PBlockToast> + <template #container="{ message }"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <!-- Можно указать через icon или через как IconCircleCheck. --> + <!-- В случае использования иконки как IconCircleCheck, то icon в конфиге не указываем. --> + <i :class="'p-toast-message-icon ti ' + message.icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">{{ message.summary }}</span> + <div class="p-toast-detail">{{ message.detail }}</div> + <div class="mt-4"> + <div class="text-sm">Дополнительный контент</div> + </div> + <div class="flex gap-2 mt-2"> + <div class="text-sm">Ячейка 1</div> + <div class="text-sm">Ячейка 2</div> + </div> + </div> + </div> + </template> + </PBlockToast> + <Button label="Показать toast" @click="showToast" /> + </div> +</template> + `, + }, + }, + }, +}; + +export const WithContentAndCloseButton = { + render: TemplateCustomContentWithCloseButton, + args: { + group: 'content-close-button', + life: 5_000, + }, + parameters: { + docs: { + source: { + language: 'html', + code: ` +<script setup lang="ts"> +import { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit'; + +const toast = usePBlockToast(); + +const showToast = () => { + toast.add({ + severity: 'info', + summary: 'Заголовок сообщения', + detail: 'Дополнительная информация', + icon: 'ti-circle-info', + life: 5_000, + }); +}; +</script> + +<template> + <div> + <PBlockToast> + <template #container="{ message, closeCallback }"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + message.icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">{{ message.summary }}</span> + <div class="p-toast-detail">{{ message.detail }}</div> + </div> + <button + class="p-button p-component p-button-text p-toast-close-button" + type="button" + @click="closeCallback" + > + <span class="p-button-icon ti ti-x"></span> + </button> + </div> + </template> + </PBlockToast> + <Button label="Показать toast" @click="showToast" /> + </div> +</template> + `, + }, + }, + }, +}; + +export const Width = { + render: TemplateWidth, + args: { + group: 'width-preview', + position: 'top-right', + life: 5_000, + }, + parameters: { + docs: { + source: { + language: 'html', + code: ` +<script setup lang="ts"> +import { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit'; + +const toast = usePBlockToast(); + +const showToast = () => { + toast.add({ + severity: 'info', + summary: 'Заголовок сообщения', + detail: 'Дополнительная информация', + life: 5_000, + }); +}; +</script> + +<template> + <div> + <PBlockToast width="sm" /> + <Button label="Показать уведомление" @click="showToast" /> + </div> +</template> + `, + }, + }, + }, +}; + +export const Position = { + render: TemplatePosition, + parameters: { + docs: { + source: { + language: 'html', + code: ` +<script setup lang="ts"> +import { PBlockToast, usePBlockToast } from '@cdek-it/vue-ui-kit'; + +const toast = usePBlockToast(); + +const showToast = (position) => { + toast.add({ + severity: 'info', + summary: 'Заголовок сообщения', + detail: 'Позиция: ' + position, + life: 5_000, + group: position, + }); +}; +</script> + +<template> + <PBlockToast position="top-left" group="top-left" /> + <PBlockToast position="top-center" group="top-center" /> + <PBlockToast position="top-right" group="top-right" /> + <PBlockToast position="bottom-left" group="bottom-left" /> + <PBlockToast position="bottom-center" group="bottom-center" /> + <PBlockToast position="bottom-right" group="bottom-right" /> + + <Button label="Вверх слева" @click="showToast('top-left')" /> + <Button label="Вверх по центру" @click="showToast('top-center')" /> + <Button label="Вверх справа" @click="showToast('top-right')" /> + <Button label="Вниз слева" @click="showToast('bottom-left')" /> + <Button label="Вниз по центру" @click="showToast('bottom-center')" /> + <Button label="Вниз справа" @click="showToast('bottom-right')" /> +</template> + `, + }, + }, + }, +}; diff --git a/src/plugins/prime/stories/Toast/Toast.template.js b/src/plugins/prime/stories/Toast/Toast.template.js new file mode 100644 index 00000000..c69a8161 --- /dev/null +++ b/src/plugins/prime/stories/Toast/Toast.template.js @@ -0,0 +1,458 @@ +import { ref } from 'vue'; +import { PBlockToast, usePBlockToast } from '@/primeBlocks'; +import Button from 'primevue/button'; + +const MessageIcons = { + success: 'ti-circle-check', + info: 'ti-info-circle', + warn: 'ti-alert-triangle', + error: 'ti-alert-circle', +}; + +const SEVERITIES = [ + { type: 'success', icon: MessageIcons.success, label: 'Успех' }, + { type: 'info', icon: MessageIcons.info, label: 'Информация' }, + { type: 'warn', icon: MessageIcons.warn, label: 'Предупреждение' }, + { type: 'error', icon: MessageIcons.error, label: 'Ошибка' }, +]; + +const POSITIONS = [ + { position: 'top-left', label: 'Вверх слева', group: 'pos-top-left' }, + { position: 'top-center', label: 'Вверх по центру', group: 'pos-top-center' }, + { position: 'top-right', label: 'Вверх справа', group: 'pos-top-right' }, + { position: 'bottom-left', label: 'Вниз слева', group: 'pos-bottom-left' }, + { + position: 'bottom-center', + label: 'Вниз по центру', + group: 'pos-bottom-center', + }, + { position: 'bottom-right', label: 'Вниз справа', group: 'pos-bottom-right' }, +]; + +const SIZES = [ + { + key: 'sm', + label: 'Small (20rem)', + width: '20rem', + group: 'width-sm', + }, + { + key: 'base', + label: 'Base (25rem)', + width: '25rem', + group: 'width-base', + }, + { + key: 'lg', + label: 'Large (30rem)', + width: '30rem', + group: 'width-lg', + }, + { + key: 'xlg', + label: 'X-Large (45rem)', + width: '45rem', + group: 'width-xlg', + }, +]; + +const commonToastConfig = { + summary: 'Заголовок сообщения', + detail: 'Дополнительная информация', +}; + +export const Template = (args) => ({ + components: { PBlockToast, Button }, + setup() { + const toast = usePBlockToast(); + + const showToast = (severity, icon) => { + toast.add({ + severity, + ...commonToastConfig, + life: args.life ?? 5_000, + icon, + group: args.group || `story_${Date.now()}`, + }); + }; + + return { + args, + showToast, + severities: SEVERITIES, + }; + }, + template: ` + <div> + <PBlockToast + :position="args.position" + :group="args.group" + :width="args.width" + style="z-index:1" + /> + + <div class="grid grid-cols-2 gap-4"> + <div v-for="({ type, icon }, index) in severities" :key="index"> + <div :class="'p-toast-message p-toast-message-' + type"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">Заголовок сообщения</span> + <div class="p-toast-detail">Дополнительная информация</div> + </div> + </div> + </div> + </div> + </div> + <br> + <hr> + <div class="flex flex-wrap gap-2 mt-6"> + <Button + v-for="({ type, icon, label }, index) in severities" + :key="index" + :label="'Показать Toast: ' + label" + :severity="type === 'error' ? 'danger' : type" + @click="showToast(type, icon)" + /> + </div> + </div> + `, +}); + +export const TemplateCloseButton = (args) => ({ + components: { PBlockToast, Button }, + setup() { + const toast = usePBlockToast(); + + const showToast = (severity, icon) => { + toast.add({ + severity, + ...commonToastConfig, + life: args.life ?? 5_000, + icon, + group: args.group || `story_${Date.now()}`, + closable: true, + }); + }; + + return { + args, + showToast, + severities: SEVERITIES, + }; + }, + template: ` + <div> + <PBlockToast + :position="args.position" + :group="args.group" + :width="args.width" + style="z-index:1" + /> + + <div class="grid grid-cols-2 gap-4"> + <div v-for="({ type, icon }, index) in severities" :key="index"> + <div :class="'p-toast-message p-toast-message-' + type"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">Заголовок сообщения</span> + <div class="p-toast-detail">Дополнительная информация</div> + </div> + <button class="p-toast-close-button" type="button"> + <i class="ti ti-x"></i> + </button> + </div> + </div> + </div> + </div> + <br> + <hr> + <div class="flex flex-wrap gap-2 mt-6"> + <Button + v-for="({ type, icon, label }, index) in severities" + :key="index" + :label="'Показать Toast: ' + label" + :severity="type === 'error' ? 'danger' : type" + @click="showToast(type, icon)" + /> + </div> + </div> + `, +}); + +export const TemplateWithContent = (args) => ({ + components: { PBlockToast, Button }, + setup() { + const toast = usePBlockToast(); + + const showToast = (severity, icon) => { + toast.add({ + severity, + ...commonToastConfig, + life: args.life ?? 5_000, + icon, + group: args.group || `story_${Date.now()}`, + }); + }; + + return { + args, + severities: SEVERITIES, + showToast, + }; + }, + template: ` + <div> + <PBlockToast :group="args.group" :width="args.width" style="z-index:1"> + <template #container="{ message }"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + message.icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">{{ message.summary }}</span> + <div class="p-toast-detail">{{ message.detail }}</div> + <div class="mt-4"> + <div class="text-sm">Дополнительный контент</div> + </div> + <div class="flex gap-2 mt-2"> + <div class="text-sm">Ячейка 1</div> + <div class="text-sm">Ячейка 2</div> + </div> + </div> + </div> + </template> + </PBlockToast> + <div class="grid grid-cols-2 gap-4"> + <div v-for="({ type, icon }, idx) in severities" :key="idx"> + <div :class="'p-toast-message p-toast-message-' + type"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">Заголовок сообщения</span> + <div class="p-toast-detail">Дополнительная информация</div> + <div class="mt-4"> + <div class="text-sm">Дополнительный контент</div> + </div> + <div class="flex gap-2 mt-2"> + <div class="text-sm">Ячейка 1</div> + <div class="text-sm">Ячейка 2</div> + </div> + </div> + </div> + </div> + </div> + </div> + <br> + <hr> + <div class="flex flex-wrap gap-2 mt-6"> + <Button + v-for="({ type, icon, label }, index) in severities" + :key="index" + :label="'Показать Toast: ' + label" + :severity="type === 'error' ? 'danger' : type" + @click="showToast(type, icon)" + /> + </div> + </div> + `, +}); + +export const TemplateCustomContentWithCloseButton = (args) => ({ + components: { PBlockToast, Button }, + setup() { + const toast = usePBlockToast(); + + const showToast = (severity, icon) => { + toast.add({ + severity, + ...commonToastConfig, + life: args.life ?? 5_000, + icon, + group: args.group, + }); + }; + + return { + args, + showToast, + severities: SEVERITIES, + }; + }, + template: ` + <div> + <PBlockToast :group="args.group" :width="args.width" style="z-index:1"> + <template #container="{ message, closeCallback }"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + message.icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">{{ message.summary }}</span> + <div class="p-toast-detail">{{ message.detail }}</div> + <div class="mt-4"> + <div class="text-sm">Дополнительный контент</div> + </div> + <div class="flex gap-2 mt-2"> + <div class="text-sm">Ячейка 1</div> + <div class="text-sm">Ячейка 2</div> + </div> + </div> + <button + class="p-button p-component p-button-text p-toast-close-button" + type="button" + @click="closeCallback" + > + <span class="p-button-icon ti ti-x"></span> + </button> + </div> + </template> + </PBlockToast> + <div class="grid grid-cols-2 gap-4"> + <div + v-for="({ type, icon }, index) in severities" + :key="index" + > + <div :class="'p-toast-message p-toast-message-' + type"> + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i :class="'p-toast-message-icon ti ' + icon"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">Заголовок сообщения</span> + <div class="p-toast-detail">Дополнительная информация</div> + <div class="mt-4"> + <div class="text-sm">Дополнительный контент</div> + </div> + <div class="flex gap-2 mt-2"> + <div class="text-sm">Ячейка 1</div> + <div class="text-sm">Ячейка 2</div> + </div> + </div> + <button class="p-toast-close-button" type="button"> + <i class="ti ti-x"></i> + </button> + </div> + </div> + </div> + </div> + <br> + <hr> + <div class="flex flex-wrap gap-2 mt-6"> + <Button + v-for="({ type, icon, label }, index) in severities" + :key="index" + :label="'Показать Toast: ' + label" + :severity="type === 'error' ? 'danger' : type" + @click="showToast(type, icon)" + /> + </div> + </div> + `, +}); + +export const TemplateWidth = (args) => ({ + components: { PBlockToast, Button }, + setup() { + const toast = usePBlockToast(); + const currentSize = ref(SIZES[1]); + const group = args?.group || 'width-preview'; + + const showToast = (size) => { + toast.removeGroup(group); + currentSize.value = size; + + toast.add({ + severity: 'info', + ...commonToastConfig, + detail: 'Ширина: ' + size.width, + life: args.life ?? 5_000, + group, + }); + }; + + return { + currentSize, + sizes: SIZES, + showToast, + }; + }, + template: ` + <div> + <PBlockToast + group="width-preview" + :width="currentSize.key" + style="z-index:1" + /> + <div class="flex flex-col gap-4"> + <div + v-for="({ key, width }, index) in sizes" + :key="index" + class="p-toast-message p-toast-message-info" + :style="'width:' + width" + > + <div class="p-toast-message-content"> + <div class="p-toast-accent-line"></div> + <i class="p-toast-message-icon ti ti-info-circle"></i> + <div class="p-toast-message-text"> + <span class="p-toast-summary">Заголовок сообщения</span> + <div class="p-toast-detail">Ширина {{ key }}: {{ width }}</div> + </div> + </div> + </div> + </div> + <div class="flex flex-wrap gap-2 mt-6"> + <Button + v-for="(size) in sizes" + :key="size.label" + :label="size.label" + @click="showToast(size)" + /> + </div> + </div> + `, +}); + +export const TemplatePosition = (args) => ({ + components: { PBlockToast, Button }, + setup() { + const toast = usePBlockToast(); + + const showToast = (group, position) => { + toast.add({ + group, + severity: 'info', + ...commonToastConfig, + detail: 'Позиция: ' + position, + life: 5_000, + }); + }; + + return { + args, + showToast, + positions: POSITIONS, + }; + }, + template: ` + <div> + <PBlockToast + v-for="({ position, group }) in positions" + :key="group" + :position="position" + :group="group" + :width="args.width" + style="z-index:1" + /> + <div class="grid grid-cols-3 gap-2"> + <Button + v-for="({ position, label, group }) in positions" + :key="group" + :label="label" + @click="showToast(group, position)" + /> + </div> + </div> + `, +}); diff --git a/src/plugins/prime/theme3.0/components/css/toast.ts b/src/plugins/prime/theme3.0/components/css/toast.ts new file mode 100644 index 00000000..9efd94fb --- /dev/null +++ b/src/plugins/prime/theme3.0/components/css/toast.ts @@ -0,0 +1,205 @@ +const css = ({ dt }: { dt: (token: string) => string }) => ` +/* Основной контейнер toast-сообщения */ +.p-toast-message { + width: ${dt('toast.root.width')}; + overflow: hidden; + border-width: ${dt('toast.root.borderWidth')}; + border-radius: ${dt('toast.root.borderRadius')}; + box-shadow: ${dt('toast.colorScheme.light.info.shadow')}; + position: relative; +} + +/* border-radius для контента toast-сообщения */ +.p-toast .p-toast-message .p-toast-message-content { + border-radius: ${dt('toast.root.borderRadius')}; +} + +/* svg-иконка */ +.p-toast .p-toast-message .p-toast-message-content .tabler-icon { + width: 2.25rem; + height: 2.25rem; + stroke: 1.5; +} + +/* Заголовок toast */ +.p-toast-summary { + line-height: ${dt('fonts.lineHeight.250')}; +} + +/* Детальное описание toast */ +.p-toast-message .p-toast-detail { + line-height: ${dt('fonts.lineHeight.250')}; +} + +/* Кнопка закрытия toast-сообщения */ +.p-toast-message .p-toast-message-content .p-toast-close-button { + margin: 0; + padding: 0; + right: 0; +} + +/* Общие стили border для кнопки закрытия всех типов toast */ +.p-toast-message-info .p-toast-close-button, +.p-toast-message-success .p-toast-close-button, +.p-toast-message-warn .p-toast-close-button, +.p-toast-message-error .p-toast-close-button { + border: ${dt('toast.extend.extCloseButton.width')} solid; +} + +/* Общие стили для акцентной линии всех типов toast */ +.p-toast-message-info .p-toast-accent-line, +.p-toast-message-success .p-toast-accent-line, +.p-toast-message-warn .p-toast-accent-line, +.p-toast-message-error .p-toast-accent-line { + width: ${dt('toast.extend.extAccentLine.width')}; + position: absolute; + left: 0; + top: 0; + bottom: 0; + border-radius: ${dt('toast.root.borderRadius')} 0 0 ${dt( + 'toast.root.borderRadius' +)}; +} + +.p-toast-close-button > i { + font-size: 1.25rem; +} + +/* Размеры тоста через классы */ +.p-toast.p-toast-sm, +.p-toast.p-toast-sm .p-toast-message { + width: ${dt('messages.sm.width')}; +} + +.p-toast.p-toast-lg, +.p-toast.p-toast-lg .p-toast-message { + width: ${dt('messages.lg.width')}; +} + +.p-toast.p-toast-xlg, +.p-toast.p-toast-xlg .p-toast-message { + width: ${dt('messages.xlg.width')}; +} + +/* Стили для toast типа Info */ +.p-toast-message-info .p-toast-message-icon, +.p-toast-message-info .p-toast-message-content .tabler-icon { + color: ${dt('toast.extend.extInfo.color')}; +} + +.p-toast-message-info .p-toast-close-button { + color: ${dt('toast.extend.extInfo.closeButton.color')}; + border-color: ${dt('toast.extend.extInfo.closeButton.borderColor')}; +} + +.p-toast-message.p-toast-message-info .p-toast-close-button.p-button-text:not(:disabled):hover { + background: ${dt('toast.colorScheme.light.info.closeButton.hoverBackground')}; + border-color: ${dt('toast.extend.extInfo.closeButton.borderColor')}; + color: ${dt('toast.extend.extInfo.closeButton.color')}; +} + +.p-toast-message-info .p-toast-accent-line { + background: ${dt('toast.extend.extInfo.color')}; +} + +.p-toast-message-info .p-toast-summary { + color: ${dt('toast.colorScheme.light.info.color')}; +} + +.p-toast-message-info .p-toast-detail { + color: ${dt('toast.colorScheme.light.info.detailColor')}; +} + +/* Стили для toast типа Success */ +.p-toast-message-success .p-toast-message-icon, +.p-toast-message-success .p-toast-message-content .tabler-icon { + color: ${dt('toast.extend.extSuccess.color')}; +} + +.p-toast-message-success .p-toast-close-button { + color: ${dt('toast.extend.extSuccess.closeButton.color')}; + border-color: ${dt('toast.extend.extSuccess.closeButton.borderColor')}; +} + +.p-toast-message.p-toast-message-success .p-toast-close-button.p-button-text:not(:disabled):hover { + background: ${dt( + 'toast.colorScheme.light.success.closeButton.hoverBackground' + )}; + border-color: ${dt('toast.extend.extSuccess.closeButton.borderColor')}; + color: ${dt('toast.extend.extSuccess.closeButton.color')}; +} + +.p-toast-message-success .p-toast-accent-line { + background: ${dt('toast.extend.extSuccess.color')}; +} + +.p-toast-message-success .p-toast-summary { + color: ${dt('toast.colorScheme.light.success.color')}; +} + +.p-toast-message-success .p-toast-detail { + color: ${dt('toast.colorScheme.light.success.detailColor')}; +} + +/* Стили для toast типа Warn */ +.p-toast-message-warn .p-toast-message-icon, +.p-toast-message-warn .p-toast-message-content .tabler-icon { + color: ${dt('toast.extend.extWarn.color')}; +} + +.p-toast-message-warn .p-toast-close-button { + color: ${dt('toast.extend.extWarn.closeButton.color')}; + border-color: ${dt('toast.extend.extWarn.closeButton.borderColor')}; +} + +.p-toast-message.p-toast-message-warn .p-toast-close-button.p-button-text:not(:disabled):hover { + background: ${dt('toast.colorScheme.light.warn.closeButton.hoverBackground')}; + border-color: ${dt('toast.extend.extWarn.closeButton.borderColor')}; + color: ${dt('toast.extend.extWarn.closeButton.color')}; +} + +.p-toast-message-warn .p-toast-accent-line { + background: ${dt('toast.extend.extWarn.color')}; +} + +.p-toast-message-warn .p-toast-summary { + color: ${dt('toast.colorScheme.light.warn.color')}; +} + +.p-toast-message-warn .p-toast-detail { + color: ${dt('toast.colorScheme.light.warn.detailColor')}; +} + +/* Стили для toast типа Error */ +.p-toast-message-error .p-toast-message-icon, +.p-toast-message-error .p-toast-message-content .tabler-icon { + color: ${dt('toast.extend.extError.color')}; +} + +.p-toast-message-error .p-toast-close-button { + color: ${dt('toast.extend.extError.closeButton.color')}; + border-color: ${dt('toast.extend.extError.closeButton.borderColor')}; +} + +.p-toast-message.p-toast-message-error .p-toast-close-button.p-button-text:not(:disabled):hover { + background: ${dt( + 'toast.colorScheme.light.error.closeButton.hoverBackground' + )}; + border-color: ${dt('toast.extend.extError.closeButton.borderColor')}; + color: ${dt('toast.extend.extError.closeButton.color')}; +} + +.p-toast-message-error .p-toast-accent-line { + background: ${dt('toast.extend.extError.color')}; +} + +.p-toast-message-error .p-toast-summary { + color: ${dt('toast.colorScheme.light.error.color')}; +} + +.p-toast-message-error .p-toast-detail { + color: ${dt('toast.colorScheme.light.error.detailColor')}; +} +`; + +export default css; diff --git a/src/plugins/prime/theme3.0/css.ts b/src/plugins/prime/theme3.0/css.ts index 55362c06..93c5498d 100644 --- a/src/plugins/prime/theme3.0/css.ts +++ b/src/plugins/prime/theme3.0/css.ts @@ -1,5 +1,8 @@ -// const css = ({ dt }: { dt: (token: string) => string }) => ` -const css = () => ` +import toastCss from './components/css/toast'; + +const css = ({ dt }: { dt: (token: string) => string }) => ` + ${toastCss({ dt })} + .p-disabled, .p-component:disabled { mix-blend-mode: luminosity; } diff --git a/src/plugins/prime/theme3.0/tokens.json b/src/plugins/prime/theme3.0/tokens.json index a7d0103e..dc7530ad 100644 --- a/src/plugins/prime/theme3.0/tokens.json +++ b/src/plugins/prime/theme3.0/tokens.json @@ -672,6 +672,18 @@ "width": "{sizing.128x}" } }, + "messages": { + "width": "{sizing.100x}", + "sm": { + "width": "{sizing.80x}" + }, + "lg": { + "width": "{sizing.120x}" + }, + "xlg": { + "width": "{sizing.128x}" + } + }, "feedback": { "transitionDuration": "{transition.duration.200}", "width": { diff --git a/src/primeBlocks/PBlockToast/PBlockToast.vue b/src/primeBlocks/PBlockToast/PBlockToast.vue new file mode 100644 index 00000000..9ff16ccb --- /dev/null +++ b/src/primeBlocks/PBlockToast/PBlockToast.vue @@ -0,0 +1,73 @@ +<script setup lang="ts"> +import { Toast, type ToastEvent, type ToastProps } from 'primevue'; +import { PBlockToastMessageIcon } from './usePBlockToast'; + +import { + IconCircleCheck, + IconInfoCircle, + IconAlertTriangle, + IconAlertCircle, +} from '@tabler/icons-vue'; + +interface IPBlockToast extends ToastProps { + width?: 'sm' | 'md' | 'lg' | 'xlg'; +} + +withDefaults(defineProps<IPBlockToast>(), { + width: 'md', +}); + +const emit = defineEmits<{ + (e: 'close', event: ToastEvent): void; + (e: 'life-end', event: ToastEvent): void; +}>(); +</script> + +<template> + <Toast + v-bind="{ ...$props, ...$attrs }" + :class="`p-toast-${width}`" + @close="emit('close', $event)" + @lifeEnd="emit('life-end', $event)" + > + <template v-if="$slots.container" #container="slotProps"> + <slot name="container" v-bind="slotProps || {}" /> + </template> + <template v-if="$slots.message" #message="slotProps"> + <slot name="message" v-bind="slotProps" /> + </template> + <template v-else #message="slotProps"> + <div class="p-toast-accent-line"></div> + <IconCircleCheck + v-if="slotProps.message.icon === PBlockToastMessageIcon.success" + /> + <IconInfoCircle + v-else-if="slotProps.message.icon === PBlockToastMessageIcon.info" + /> + <IconAlertTriangle + v-else-if="slotProps.message.icon === PBlockToastMessageIcon.warn" + /> + <IconAlertCircle + v-else-if="slotProps.message.icon === PBlockToastMessageIcon.error" + /> + <i + v-else + :class="`p-icon p-toast-message-icon ti ${slotProps.message.icon}`" + /> + <div class="p-toast-message-text"> + <span class="p-toast-summary"> + {{ slotProps.message.summary }} + </span> + <div class="p-toast-detail"> + {{ slotProps.message.detail }} + </div> + </div> + </template> + <template v-if="$slots.messageicon" #messageicon> + <slot name="messageicon" /> + </template> + <template v-if="$slots.closeicon" #closeicon> + <slot name="closeicon" /> + </template> + </Toast> +</template> diff --git a/src/primeBlocks/PBlockToast/usePBlockToast.ts b/src/primeBlocks/PBlockToast/usePBlockToast.ts new file mode 100644 index 00000000..7a93254f --- /dev/null +++ b/src/primeBlocks/PBlockToast/usePBlockToast.ts @@ -0,0 +1,42 @@ +import type { HintedString } from '@primevue/core'; +import { useToast } from 'primevue/usetoast'; +import type { ToastMessageOptions } from 'primevue/toast'; + +export const PBlockToastMessageIcon = { + success: 'success', + info: 'info', + warn: 'warn', + error: 'error', +} as const; + +export type PBlockToastMessageIconValues = + (typeof PBlockToastMessageIcon)[keyof typeof PBlockToastMessageIcon]; + +export interface PBlockToastMessageOptions extends ToastMessageOptions { + severity?: HintedString<PBlockToastMessageIconValues>; + icon?: string; +} + +export function usePBlockToast() { + const toast = useToast(); + + const add = (config: PBlockToastMessageOptions) => { + const severity = config.severity || 'info'; + const icon: string | PBlockToastMessageIconValues = + config?.icon || severity; + + toast.add({ + ...config, + closable: config?.closable || false, + // @ts-ignore + icon, + }); + }; + + return { + add, + remove: toast.remove, + removeGroup: toast.removeGroup, + removeAllGroups: toast.removeAllGroups, + }; +} diff --git a/src/primeBlocks/index.ts b/src/primeBlocks/index.ts index c6023657..86606279 100644 --- a/src/primeBlocks/index.ts +++ b/src/primeBlocks/index.ts @@ -2,5 +2,16 @@ import PBlockPassword from './PBlockExample/PBlockPassword.vue'; import PBlockMenubar from './PBlockMenubar/PBlockMenubar.vue'; import PBlockMenuItem from './PBlockMenuItem/PBlockMenuItem.vue'; import PBlockToggleButton from './PBlockToggleButton/PBlockToggleButton.vue'; +import PBlockToast from './PBlockToast/PBlockToast.vue'; -export { PBlockPassword, PBlockMenubar, PBlockMenuItem, PBlockToggleButton }; +export { + PBlockPassword, + PBlockMenubar, + PBlockMenuItem, + PBlockToggleButton, + PBlockToast, +}; +export { + usePBlockToast, + PBlockToastMessageIcon, +} from './PBlockToast/usePBlockToast'; diff --git a/yarn.lock b/yarn.lock index ec05e009..def5e514 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1294,6 +1294,13 @@ type-fest "~2.19" vue-component-type-helpers latest +"@tabler/icons-vue@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@tabler/icons-vue/-/icons-vue-3.22.0.tgz#3919c344b22f9dfe7087be5d640a62262156dc4a" + integrity sha512-cgjvq+kjfqu7aJCAUmZByf+enUeLhSI7gVyWzBMCKNCehPoUHBmWNqM5zdnVJw/X2ogdvYL/H5arrSvKktH70Q== + dependencies: + "@tabler/icons" "3.22.0" + "@tabler/icons-webfont@^3.22.0": version "3.34.1" resolved "https://registry.npmjs.org/@tabler/icons-webfont/-/icons-webfont-3.34.1.tgz" @@ -1302,6 +1309,11 @@ "@tabler/icons" "3.34.1" sharp "^0.34.1" +"@tabler/icons@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.22.0.tgz#f937ec65d98710b891da6368559ea15cbcd87f91" + integrity sha512-IfgGzhFph5OBr2wTieWL/hyAs0FThnq9O155a6kfGYxqx7h5LQw91wnRswhEaGhXCcfmR7ZVDUr9H+x4b9Pb8g== + "@tabler/icons@3.34.1": version "3.34.1" resolved "https://registry.npmjs.org/@tabler/icons/-/icons-3.34.1.tgz"