enhance(ui): restyle toasts (#38842)

Restyle toasts, add success toast to match the alerts.

<img width="500" alt="Screenshot 2026-08-09 at 11 33 44"
src="https://github.com/user-attachments/assets/d36422b8-0030-41dd-8056-b2d10d86280d"
/>

Alternatively we could also tint to match the alerts, but I do prefer
untinted.

<img width="500" alt="Screenshot 2026-08-09 at 11 34 44"
src="https://github.com/user-attachments/assets/73fcfde4-618b-424f-9aa9-4ada5c2904f6"
/>

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
silverwind
2026-08-09 22:17:28 +02:00
committed by GitHub
co-authored by Giteabot
parent 85558c28fc
commit 7ac505ff0d
7 changed files with 37 additions and 20 deletions
+1
View File
@@ -4,6 +4,7 @@
<div class="ui container"> <div class="ui container">
<h1>Toast</h1> <h1>Toast</h1>
<div> <div>
<button class="ui button toast-test-button" data-toast-level="success" data-toast-message="test success">Show Success Toast</button>
<button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button> <button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button>
<button class="ui button toast-test-button" data-toast-level="warning" data-toast-message="test warning">Show Warning Toast</button> <button class="ui button toast-test-button" data-toast-level="warning" data-toast-message="test warning">Show Warning Toast</button>
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="test error">Show Error Toast</button> <button class="ui button toast-test-button" data-toast-level="error" data-toast-message="test error">Show Error Toast</button>
+8 -1
View File
@@ -1,5 +1,7 @@
.toastify { .toastify {
color: var(--color-white); color: var(--color-text);
background: var(--color-menu);
border: 1px solid var(--color-secondary);
position: fixed; position: fixed;
opacity: 0; opacity: 0;
transition: all .2s ease; transition: all .2s ease;
@@ -16,6 +18,11 @@
opacity: 1; opacity: 1;
} }
.toast-success .toast-icon { color: var(--color-success-text); }
.toast-info .toast-icon { color: var(--color-info-text); }
.toast-warning .toast-icon { color: var(--color-warning-text); }
.toast-error .toast-icon { color: var(--color-error-text); }
.toast-body { .toast-body {
flex: 1; flex: 1;
padding: 5px 0; padding: 5px 0;
+4 -6
View File
@@ -1,21 +1,19 @@
import {showInfoToast, showWarningToast, showErrorToast} from './toast.ts'; import {showSuccessToast, showInfoToast, showWarningToast, showErrorToast} from './toast.ts';
import type {Toast} from './toast.ts';
import {registerGlobalInitFunc} from './observer.ts'; import {registerGlobalInitFunc} from './observer.ts';
import {showFomanticModal} from './fomantic/modal.ts'; import {showFomanticModal} from './fomantic/modal.ts';
import {createElementFromHTML} from '../utils/dom.ts'; import {createElementFromHTML} from '../utils/dom.ts';
import {html} from '../utils/html.ts'; import {html} from '../utils/html.ts';
import {showGlobalErrorMessage} from './errors.ts'; import {showGlobalErrorMessage} from './errors.ts';
import {AnsiLineRenderer} from '../render/ansi.ts'; import {AnsiLineRenderer} from '../render/ansi.ts';
import type {Intent} from '../types.ts';
type LevelMap = Record<string, (message: string) => Toast | null>;
function initDevtestPage() { function initDevtestPage() {
const toastButtons = document.querySelectorAll('.toast-test-button'); const toastButtons = document.querySelectorAll('.toast-test-button');
if (toastButtons.length) { if (toastButtons.length) {
const levelMap: LevelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast}; const levelMap = {success: showSuccessToast, info: showInfoToast, warning: showWarningToast, error: showErrorToast};
for (const el of toastButtons) { for (const el of toastButtons) {
el.addEventListener('click', () => { el.addEventListener('click', () => {
const level = el.getAttribute('data-toast-level')!; const level = el.getAttribute('data-toast-level') as Intent;
const message = el.getAttribute('data-toast-message')!; const message = el.getAttribute('data-toast-message')!;
levelMap[level](message); levelMap[level](message);
}); });
+9 -4
View File
@@ -1,16 +1,21 @@
import {showInfoToast, showErrorToast, showWarningToast} from './toast.ts'; import {showSuccessToast, showInfoToast, showErrorToast, showWarningToast} from './toast.ts';
test('showSuccessToast', async () => {
showSuccessToast('success', {duration: -1});
expect(document.querySelector('.toastify')).toBeTruthy();
});
test('showInfoToast', async () => { test('showInfoToast', async () => {
showInfoToast('success 😀', {duration: -1}); showInfoToast('info', {duration: -1});
expect(document.querySelector('.toastify')).toBeTruthy(); expect(document.querySelector('.toastify')).toBeTruthy();
}); });
test('showWarningToast', async () => { test('showWarningToast', async () => {
showWarningToast('warning 😐', {duration: -1}); showWarningToast('warning', {duration: -1});
expect(document.querySelector('.toastify')).toBeTruthy(); expect(document.querySelector('.toastify')).toBeTruthy();
}); });
test('showErrorToast', async () => { test('showErrorToast', async () => {
showErrorToast('error 🙁', {duration: -1}); showErrorToast('error', {duration: -1});
expect(document.querySelector('.toastify')).toBeTruthy(); expect(document.querySelector('.toastify')).toBeTruthy();
}); });
+12 -8
View File
@@ -12,25 +12,25 @@ export type Toast = ReturnType<typeof StartToastifyInstance>;
type ToastLevels = { type ToastLevels = {
[intent in Intent]: { [intent in Intent]: {
icon: SvgName, icon: SvgName,
background: string,
duration: number, duration: number,
} }
}; };
const levels: ToastLevels = { const levels: ToastLevels = {
info: { success: {
icon: 'octicon-check', icon: 'octicon-check',
background: 'var(--color-green)',
duration: 2500, duration: 2500,
}, },
info: {
icon: 'octicon-info',
duration: 5000,
},
warning: { warning: {
icon: 'gitea-exclamation', icon: 'gitea-exclamation',
background: 'var(--color-orange)',
duration: -1, // requires dismissal to hide duration: -1, // requires dismissal to hide
}, },
error: { error: {
icon: 'gitea-exclamation', icon: 'gitea-exclamation',
background: 'var(--color-red)',
duration: -1, // requires dismissal to hide duration: -1, // requires dismissal to hide
}, },
}; };
@@ -43,7 +43,7 @@ type ToastOpts = {
type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast}; type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast};
/** See https://github.com/apvarun/toastify-js#api for options */ /** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null { function showToast(message: string, level: Intent = 'info', {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null {
const parent = document.querySelector('.ui.dimmer.active') ?? document.body; const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
const duplicateKey = preventDuplicates ? (typeof preventDuplicates === 'string' ? preventDuplicates : `${level}-${message}`) : ''; const duplicateKey = preventDuplicates ? (typeof preventDuplicates === 'string' ? preventDuplicates : `${level}-${message}`) : '';
@@ -59,7 +59,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
} }
} }
const {icon, background, duration: levelDuration} = levels[level ?? 'info']; const {icon, duration: levelDuration} = levels[level];
const bodyHtml = useHtmlBody ? message : htmlEscape(message); const bodyHtml = useHtmlBody ? message : htmlEscape(message);
const toast = Toastify({ const toast = Toastify({
selector: parent, selector: parent,
@@ -69,10 +69,10 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
<button class='btn toast-close'>${svgRaw('octicon-x')}</button> <button class='btn toast-close'>${svgRaw('octicon-x')}</button>
`, `,
escapeMarkup: false, escapeMarkup: false,
className: `toast-${level}`,
gravity: gravity ?? 'top', gravity: gravity ?? 'top',
position: position ?? 'center', position: position ?? 'center',
duration: duration ?? levelDuration, duration: duration ?? levelDuration,
style: {background},
...other, ...other,
}); });
@@ -84,6 +84,10 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
return toast; return toast;
} }
export function showSuccessToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'success', opts);
}
export function showInfoToast(message: string, opts?: ToastOpts): Toast | null { export function showInfoToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'info', opts); return showToast(message, 'info', opts);
} }
+2
View File
@@ -50,6 +50,7 @@ import octiconHistory from '../../public/assets/img/svg/octicon-history.svg';
import octiconHorizontalRule from '../../public/assets/img/svg/octicon-horizontal-rule.svg'; import octiconHorizontalRule from '../../public/assets/img/svg/octicon-horizontal-rule.svg';
import octiconHome from '../../public/assets/img/svg/octicon-home.svg'; import octiconHome from '../../public/assets/img/svg/octicon-home.svg';
import octiconImage from '../../public/assets/img/svg/octicon-image.svg'; import octiconImage from '../../public/assets/img/svg/octicon-image.svg';
import octiconInfo from '../../public/assets/img/svg/octicon-info.svg';
import octiconIssueClosed from '../../public/assets/img/svg/octicon-issue-closed.svg'; import octiconIssueClosed from '../../public/assets/img/svg/octicon-issue-closed.svg';
import octiconIssueOpened from '../../public/assets/img/svg/octicon-issue-opened.svg'; import octiconIssueOpened from '../../public/assets/img/svg/octicon-issue-opened.svg';
import octiconItalic from '../../public/assets/img/svg/octicon-italic.svg'; import octiconItalic from '../../public/assets/img/svg/octicon-italic.svg';
@@ -139,6 +140,7 @@ const svgs = {
'octicon-horizontal-rule': octiconHorizontalRule, 'octicon-horizontal-rule': octiconHorizontalRule,
'octicon-home': octiconHome, 'octicon-home': octiconHome,
'octicon-image': octiconImage, 'octicon-image': octiconImage,
'octicon-info': octiconInfo,
'octicon-issue-closed': octiconIssueClosed, 'octicon-issue-closed': octiconIssueClosed,
'octicon-issue-opened': octiconIssueOpened, 'octicon-issue-opened': octiconIssueOpened,
'octicon-italic': octiconItalic, 'octicon-italic': octiconItalic,
+1 -1
View File
@@ -1,7 +1,7 @@
export type TimeoutId = ReturnType<typeof setTimeout>; export type TimeoutId = ReturnType<typeof setTimeout>;
export type IntervalId = ReturnType<typeof setInterval>; export type IntervalId = ReturnType<typeof setInterval>;
export type Intent = 'error' | 'warning' | 'info'; export type Intent = 'error' | 'warning' | 'info' | 'success';
export type Mention = { export type Mention = {
key: string, key: string,