vanilla-jui 1.7.30 → 1.7.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/index.d.ts +276 -334
  2. package/dist/index.js +1 -1
  3. package/dist/index.umd.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/package.json +8 -4
  6. package/src/components/accordion.ts +22 -8
  7. package/src/components/flow.ts +1 -1
  8. package/src/components/form.ts +1 -1
  9. package/src/components/modal.ts +1 -1
  10. package/src/components/offcanvas.ts +2 -2
  11. package/src/components/pagination.ts +5 -5
  12. package/src/components/tabs.ts +132 -16
  13. package/src/css/animation.css +7 -3
  14. package/src/css/card.css +10 -10
  15. package/src/css/font.css +3 -6
  16. package/src/css/icon.css +3 -20
  17. package/src/css/layout.css +11 -18
  18. package/src/css/status.css +39 -3
  19. package/src/css/themes/default/accordion.css +13 -45
  20. package/src/css/themes/default/avatar.css +2 -3
  21. package/src/css/themes/default/badge.css +14 -22
  22. package/src/css/themes/default/breadcrumb.css +11 -12
  23. package/src/css/themes/default/button.css +101 -159
  24. package/src/css/themes/default/editor.css +36 -36
  25. package/src/css/themes/default/flow.css +4 -4
  26. package/src/css/themes/default/form.css +110 -217
  27. package/src/css/themes/default/loading.css +48 -63
  28. package/src/css/themes/default/menu.css +17 -17
  29. package/src/css/themes/default/modal.css +9 -9
  30. package/src/css/themes/default/offcanvas.css +3 -3
  31. package/src/css/themes/default/swiper.css +18 -18
  32. package/src/css/themes/default/tabs.css +38 -24
  33. package/src/css/themes/default/theme-palette.css +22 -18
  34. package/src/css/themes/default/tip.css +24 -13
  35. package/src/css/themes/default/toast.css +65 -45
  36. package/src/css/themes/default/toolbar.css +2 -2
  37. package/src/css/themes/default/tooltip.css +3 -4
  38. package/src/css/tokens.css +89 -59
  39. package/src/css/tokens_/345/211/257/346/234/254.css +4 -4
  40. package/src/icons/index.ts +9 -7
  41. package/src/locales/index.ts +3 -1
  42. package/src/primitives/theme.ts +7 -10
  43. package/src/primitives/toast.ts +191 -107
  44. package/src/primitives/tooltip.ts +4 -4
  45. package/src/utilities/browser.ts +1 -1
@@ -3,6 +3,7 @@ import {
3
3
  access,
4
4
  createEffect,
5
5
  createRoot,
6
+ createSignal,
6
7
  insert,
7
8
  jsx,
8
9
  } from 'vanilla-signal';
@@ -23,6 +24,7 @@ export interface ToastClassNames {
23
24
  toast: string;
24
25
  icon: string;
25
26
  message: string;
27
+ description: string;
26
28
  lite: string;
27
29
  confirm: string;
28
30
  buttons: string;
@@ -46,35 +48,55 @@ export interface ToastThemeOptions extends ToastClassNameOptions {
46
48
  }
47
49
 
48
50
  export interface ToastOptions extends ToastThemeOptions {
51
+ description?: string;
49
52
  duration?: number;
50
53
  once?: boolean;
54
+ onClose?: () => void | Promise<void>;
55
+ }
56
+
57
+ export interface LoadingToastOptions extends ToastOptions {
51
58
  loading?: MaybeAccessor<boolean>;
52
59
  text?: {
53
60
  loading?: string;
54
61
  };
55
- onClose?: () => void | Promise<void>;
56
62
  onCancel?: () => void | Promise<void>;
57
63
  }
58
64
 
59
- export interface ToastConfirmProps extends ToastClassNameOptions {
60
- once?: boolean;
65
+ export interface ConfirmToastOptions extends ToastOptions {
61
66
  text?: {
62
67
  close?: string;
63
68
  confirm?: string;
64
69
  };
65
70
  onConfirm?: () => void | Promise<void>;
71
+ }
72
+
73
+ export type ToastConfirmProps = ConfirmToastOptions;
74
+
75
+ export interface PromiseToastOptions extends ToastClassNameOptions {
76
+ duration?: number;
77
+ text?: { loading?: string; success?: string; error?: string };
78
+ onCancel?: () => void | Promise<void>;
66
79
  onClose?: () => void | Promise<void>;
80
+ onSuccess?: () => void | Promise<void>;
81
+ onError?: () => void | Promise<void>;
82
+ }
83
+
84
+ interface ToastStatus {
85
+ message: string;
86
+ theme: ToastTheme;
87
+ loading: boolean;
67
88
  }
68
89
 
69
90
  const DEFAULT_CLASS_NAMES: ToastClassNames = {
70
91
  container: 'j-toast-container',
71
92
  toast: 'j-toast',
72
93
  icon: 'el-icon',
73
- message: 'el-text',
94
+ message: 'toast-title',
95
+ description: 'toast-description',
74
96
  lite: 'j-toast-lite',
75
97
  confirm: 'j-toast is-confirm',
76
- buttons: 'toast-buttons',
77
- button: 'j-button is-sm',
98
+ buttons: 'toast-btns',
99
+ button: 'j-btn is-sm',
78
100
  closeBtn: 'is-ghost',
79
101
  confirmBtn: 'is-reverse',
80
102
  info: 'is-info',
@@ -94,7 +116,8 @@ const timers = new Set<string>();
94
116
  const disposers = new Map<HTMLElement, () => void>();
95
117
  const animations = new Map<HTMLElement, Animation>();
96
118
  const operations = new Map<HTMLElement, number>();
97
- const singletonToasts = new Map<'show' | 'confirm', HTMLElement>();
119
+ type ToastKind = 'show' | 'confirm' | 'loading';
120
+ const singletonToasts = new Map<ToastKind, HTMLElement>();
98
121
  let classNames = DEFAULT_CLASS_NAMES;
99
122
 
100
123
  function addToastDisposer(element: HTMLElement, dispose: () => void): void {
@@ -105,7 +128,7 @@ function addToastDisposer(element: HTMLElement, dispose: () => void): void {
105
128
  });
106
129
  }
107
130
 
108
- function getSingletonToast(kind: 'show' | 'confirm'): HTMLElement | null {
131
+ function getSingletonToast(kind: ToastKind): HTMLElement | null {
109
132
  const toast = singletonToasts.get(kind);
110
133
  if (!toast) return null;
111
134
  if (!toast.isConnected || toast.getAttribute('data-mount') === 'false') {
@@ -116,7 +139,7 @@ function getSingletonToast(kind: 'show' | 'confirm'): HTMLElement | null {
116
139
  }
117
140
 
118
141
  function setSingletonToast(
119
- kind: 'show' | 'confirm',
142
+ kind: ToastKind,
120
143
  toast: HTMLElement,
121
144
  once: boolean
122
145
  ): void {
@@ -129,77 +152,20 @@ function clearSingletonToast(toast: HTMLElement): void {
129
152
  }
130
153
  }
131
154
 
132
- function isToastLoading(options: ToastOptions): boolean {
133
- return !!access(options.loading ?? false);
134
- }
135
-
136
- function toastIconStyle(options: ToastOptions): Record<string, string> {
137
- return isToastLoading(options)
155
+ function toastIconStyle(loading: boolean): Record<string, string> {
156
+ return loading
138
157
  ? {
139
158
  position: 'relative',
140
159
  display: 'inline-flex',
141
160
  alignItems: 'center',
142
161
  justifyContent: 'center',
143
- width: 'var(--toast-icon-size, 1em)',
144
- height: 'var(--toast-icon-size, 1em)',
162
+ width: '1.25rem',
163
+ height: '1.25rem',
145
164
  flex: '0 0 auto',
146
165
  }
147
166
  : {};
148
167
  }
149
168
 
150
- function toastIconContent(
151
- theme: ToastTheme,
152
- options: ToastOptions
153
- ): HTMLElement | SVGElement | null {
154
- if (isToastLoading(options)) return createLoading();
155
- return theme ? icon(theme) : null;
156
- }
157
-
158
- function toastMessage(message: string, options: ToastOptions): string {
159
- return isToastLoading(options)
160
- ? options.text?.loading || translate('Loading...')
161
- : message;
162
- }
163
-
164
- async function closeByUser(
165
- toast: HTMLElement,
166
- options: ToastOptions
167
- ): Promise<void> {
168
- const shouldCancel = isToastLoading(options);
169
- hide(toast);
170
- if (shouldCancel) await options.onCancel?.();
171
- await options.onClose?.();
172
- }
173
-
174
- function bindToastStatus(
175
- element: HTMLElement,
176
- iconElement: HTMLElement,
177
- messageElement: HTMLElement,
178
- message: string,
179
- theme: ToastTheme,
180
- options: ToastOptions,
181
- duration = 0
182
- ): void {
183
- const id = element.dataset.toast || randomId();
184
- const dispose = createRoot((dispose) => {
185
- insert(
186
- element,
187
- () => (theme || isToastLoading(options) ? iconElement : null),
188
- messageElement
189
- );
190
- insert(iconElement, () => toastIconContent(theme, options));
191
- insert(messageElement, () => toastMessage(message, options));
192
- createEffect(() => {
193
- cancelToastTimers(id);
194
- if (duration > 0 && !isToastLoading(options)) {
195
- setToastTimer(id, 'hide', () => hide(element), duration);
196
- }
197
- });
198
- return dispose;
199
- });
200
- addToastDisposer(element, dispose);
201
- }
202
-
203
169
  function getOrCreateContainer(names: ToastClassNames): HTMLElement {
204
170
  let container = q<HTMLElement>('[data-toast-container]');
205
171
  if (!container) {
@@ -357,10 +323,16 @@ function hide(toast: HTMLElement | null | undefined): void {
357
323
  });
358
324
  }
359
325
 
360
- function show(message = '', options: ToastOptions = {}): HTMLElement {
326
+ function createToast(
327
+ message: string,
328
+ options: ToastOptions,
329
+ status: () => ToastStatus,
330
+ kind: ToastKind = 'show',
331
+ onCancel?: () => void | Promise<void>
332
+ ): HTMLElement {
361
333
  const once = options.once ?? false;
362
334
  if (once) {
363
- const toast = getSingletonToast('show');
335
+ const toast = getSingletonToast(kind);
364
336
  if (toast) return toast;
365
337
  }
366
338
  validateParam('message', message, 'string', 'Toast.show');
@@ -374,42 +346,140 @@ function show(message = '', options: ToastOptions = {}): HTMLElement {
374
346
  classNames,
375
347
  options.className ?? {}
376
348
  ) as ToastClassNames;
377
- const id = randomId();
378
- const iconElement = jsx('span', {
379
- className: names.icon,
380
- 'data-toast-icon': id,
381
- style: () => toastIconStyle(options),
382
- }) as HTMLElement;
383
- const messageElement = jsx('span', {
384
- className: names.message,
385
- 'data-toast-message': id,
386
- }) as HTMLElement;
387
- const toast = jsx('div', {
388
- className: joinClasses(names.toast, theme ? names[theme] : ''),
389
- 'data-toast': id,
390
- role: 'alert',
391
- 'aria-atomic': 'true',
392
- children: messageElement,
393
- }) as HTMLElement;
394
- bindToastStatus(
395
- toast,
396
- iconElement,
397
- messageElement,
349
+ return createRoot((dispose) => {
350
+ const id = randomId();
351
+ const iconElement = jsx('span', {
352
+ className: names.icon,
353
+ 'data-toast-icon': id,
354
+ style: () => toastIconStyle(status().loading),
355
+ children: () =>
356
+ status().loading
357
+ ? createLoading()
358
+ : status().theme
359
+ ? icon(status().theme)
360
+ : null,
361
+ }) as HTMLElement;
362
+ const messageElement = jsx('span', {
363
+ className: names.message,
364
+ 'data-toast-message': id,
365
+ children: () => status().message,
366
+ }) as HTMLElement;
367
+ const description = options.description ?? '';
368
+ const descriptionElement = description
369
+ ? jsx('span', {
370
+ className: names.description,
371
+ 'data-toast-description': id,
372
+ children: description,
373
+ })
374
+ : null;
375
+ const toast = jsx('div', {
376
+ className: () =>
377
+ joinClasses(
378
+ names.toast,
379
+ status().theme ? names[status().theme as Exclude<ToastTheme, ''>] : ''
380
+ ),
381
+ 'data-toast': id,
382
+ role: 'alert',
383
+ 'aria-atomic': 'true',
384
+ 'aria-live': () => (status().theme === 'error' ? 'assertive' : 'polite'),
385
+ children: [messageElement, descriptionElement],
386
+ }) as HTMLElement;
387
+ insert(
388
+ toast,
389
+ () => (status().theme || status().loading ? iconElement : null),
390
+ messageElement
391
+ );
392
+ createEffect(() => {
393
+ cancelToastTimers(id);
394
+ if (duration > 0 && !status().loading)
395
+ setToastTimer(id, 'hide', () => hide(toast), duration);
396
+ });
397
+ addToastDisposer(toast, dispose);
398
+ mountToast(
399
+ toast,
400
+ () => insert(getOrCreateContainer(names), toast),
401
+ status().theme === 'error' ? 'assertive' : 'polite'
402
+ );
403
+ setSingletonToast(kind, toast, once);
404
+ addToastDisposer(
405
+ toast,
406
+ listen(toast, 'click', () => {
407
+ const pending = status().loading;
408
+ hide(toast);
409
+ void (async () => {
410
+ if (pending) await onCancel?.();
411
+ await options.onClose?.();
412
+ })();
413
+ })
414
+ );
415
+ return toast;
416
+ });
417
+ }
418
+
419
+ function show(message = '', options: ToastOptions = {}): HTMLElement {
420
+ return createToast(message, options, () => ({
421
+ message,
422
+ theme: options.theme ?? '',
423
+ loading: false,
424
+ }));
425
+ }
426
+
427
+ const LOADING_MESSAGE = translate('Loading') + '...';
428
+
429
+ function loading(message = '', options: LoadingToastOptions = {}): HTMLElement {
430
+ return createToast(
398
431
  message,
399
- theme,
400
432
  options,
401
- duration
402
- );
403
- mountToast(
404
- toast,
405
- () => insert(getOrCreateContainer(names), toast),
406
- theme === 'error' ? 'assertive' : 'polite'
407
- );
408
- setSingletonToast('show', toast, once);
409
- addToastDisposer(
410
- toast,
411
- listen(toast, 'click', () => void closeByUser(toast, options))
433
+ () => {
434
+ const pending = !!access(options.loading ?? false);
435
+ return {
436
+ message: pending ? (options.text?.loading ?? LOADING_MESSAGE) : message,
437
+ theme: pending ? '' : 'success',
438
+ loading: pending,
439
+ };
440
+ },
441
+ 'loading',
442
+ options.onCancel
412
443
  );
444
+ }
445
+
446
+ function promise<T>(
447
+ task: Promise<T>,
448
+ options: PromiseToastOptions = {}
449
+ ): HTMLElement {
450
+ const [status, setStatus] = createSignal<ToastStatus>({
451
+ message: options.text?.loading ?? LOADING_MESSAGE,
452
+ theme: '',
453
+ loading: true,
454
+ });
455
+ const toast = createToast('', options, status, 'show', options.onCancel);
456
+ let disposed = false;
457
+ addToastDisposer(toast, () => {
458
+ disposed = true;
459
+ });
460
+ // Observe both outcomes even after dismissal so a late rejection is consumed.
461
+ void task
462
+ .then(
463
+ async () => {
464
+ if (disposed) return;
465
+ setStatus({
466
+ message: options.text?.success ?? translate('Success'),
467
+ theme: 'success',
468
+ loading: false,
469
+ });
470
+ await options.onSuccess?.();
471
+ },
472
+ async () => {
473
+ if (disposed) return;
474
+ setStatus({
475
+ message: options.text?.error ?? translate('Error'),
476
+ theme: 'error',
477
+ loading: false,
478
+ });
479
+ await options.onError?.();
480
+ }
481
+ )
482
+ .catch(console.error);
413
483
  return toast;
414
484
  }
415
485
 
@@ -443,7 +513,7 @@ function lite(
443
513
  return element;
444
514
  }
445
515
 
446
- function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
516
+ function confirm(message = '', props: ConfirmToastOptions = {}): HTMLElement {
447
517
  const once = props.once ?? true;
448
518
  if (once) {
449
519
  const toast = getSingletonToast('confirm');
@@ -453,13 +523,18 @@ function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
453
523
  const closeText = props.text?.close || translate('Close');
454
524
  const confirmText = props.text?.confirm || translate('Confirm');
455
525
  validateParam('once', once, 'boolean', 'Toast.confirm');
526
+ const duration = props.duration ?? 3000;
527
+ const theme = props.theme ?? '';
528
+ validateParam('duration', duration, TOAST_DURATION_RULE, 'Toast.confirm');
529
+ validateParam('theme', theme, TOAST_THEME_RULE, 'Toast.confirm');
530
+ const description = props.description ?? '';
456
531
  const names = mergeShallowConfig(
457
532
  classNames,
458
533
  props.className ?? {}
459
534
  ) as ToastClassNames;
460
535
  const id = randomId();
461
536
  const element = jsx('div', {
462
- className: names.confirm,
537
+ className: joinClasses(names.confirm, theme ? names[theme] : ''),
463
538
  'data-toast': id,
464
539
  'data-toast-confirm': '',
465
540
  children: [
@@ -468,6 +543,12 @@ function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
468
543
  'data-toast-message': id,
469
544
  children: message,
470
545
  }),
546
+ description
547
+ ? jsx('div', {
548
+ className: names.description,
549
+ children: description,
550
+ })
551
+ : null,
471
552
  jsx('div', {
472
553
  className: names.buttons,
473
554
  'data-toast-buttons': id,
@@ -505,6 +586,7 @@ function confirm(message = '', props: ToastConfirmProps = {}): HTMLElement {
505
586
  }
506
587
  );
507
588
  setSingletonToast('confirm', element, once);
589
+ if (duration > 0) setToastTimer(id, 'hide', () => hide(element), duration);
508
590
  return element;
509
591
  }
510
592
 
@@ -532,6 +614,8 @@ export const Toast = {
532
614
  return { className: classNames };
533
615
  },
534
616
  show,
617
+ loading,
618
+ promise,
535
619
  info: (message = '', options: ToastOptions = {}) =>
536
620
  show(message, { ...options, theme: 'info' }),
537
621
  success: (message = '', options: ToastOptions = {}) =>
@@ -39,7 +39,7 @@ interface TooltipClassNames {
39
39
 
40
40
  interface TooltipThemeClassNames {
41
41
  reverse: string;
42
- primary: string;
42
+ info: string;
43
43
  success: string;
44
44
  warning: string;
45
45
  error: string;
@@ -48,7 +48,7 @@ interface TooltipThemeClassNames {
48
48
  type TooltipTheme =
49
49
  | false
50
50
  | 'reverse'
51
- | 'primary'
51
+ | 'info'
52
52
  | 'success'
53
53
  | 'warning'
54
54
  | 'error';
@@ -95,7 +95,7 @@ const DEFAULT_CLASS_NAMES: TooltipClassNames = {
95
95
  message: 'el-text',
96
96
  ui: {
97
97
  reverse: 'is-reverse',
98
- primary: 'is-primary',
98
+ info: 'is-info',
99
99
  success: 'is-success',
100
100
  warning: 'is-warning',
101
101
  error: 'is-error',
@@ -131,7 +131,7 @@ const TOOLTIP_OPTIONS_SCHEMA = {
131
131
  theme: {
132
132
  default: false,
133
133
  types: ['string', 'boolean'],
134
- enum: [false, 'reverse', 'primary', 'success', 'warning', 'error'],
134
+ enum: [false, 'reverse', 'info', 'success', 'warning', 'error'],
135
135
  },
136
136
  cache: { default: false, type: 'boolean' },
137
137
  ttl: {
@@ -132,7 +132,7 @@ export function checkModernBrowser(): SupportES2022 {
132
132
  window.open('https://www.google.cn/chrome/fallback/', '_blank');
133
133
  },
134
134
  className: {
135
- confirmBtn: 'j-button is-sm is-danger',
135
+ confirmBtn: 'j-btn is-sm is-danger',
136
136
  },
137
137
  });
138
138
  return false;