@immich/ui 0.37.1 → 0.38.0

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.
@@ -3,6 +3,7 @@
3
3
  import Icon from '../Icon/Icon.svelte';
4
4
  import Label from '../Label/Label.svelte';
5
5
  import Text from '../Text/Text.svelte';
6
+ import { styleVariants } from '../../styles.js';
6
7
  import type { Color, Shape, Size } from '../../types.js';
7
8
  import { cleanClass, generateId } from '../../utilities/internal.js';
8
9
  import { mdiCheck, mdiMinus } from '@mdi/js';
@@ -30,19 +31,8 @@
30
31
  const containerStyles = tv({
31
32
  base: 'ring-offset-background focus-visible:ring-ring peer data-[state=checked]:bg-primary box-content overflow-hidden border-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50',
32
33
  variants: {
33
- shape: {
34
- rectangle: 'rounded-none',
35
- 'semi-round': 'rounded-lg',
36
- round: 'rounded-full',
37
- },
38
- color: {
39
- primary: 'border-primary',
40
- secondary: 'border-dark',
41
- success: 'border-success',
42
- danger: 'border-danger',
43
- warning: 'border-warning',
44
- info: 'border-info',
45
- },
34
+ shape: styleVariants.shape,
35
+ borderColor: styleVariants.borderColor,
46
36
  size: {
47
37
  tiny: 'size-4',
48
38
  small: 'size-5',
@@ -65,14 +55,8 @@
65
55
  fullWidth: {
66
56
  true: 'w-full',
67
57
  },
68
- color: {
69
- primary: 'bg-primary text-light hover:bg-primary/80',
70
- secondary: 'bg-dark text-light hover:bg-dark/80',
71
- success: 'bg-success text-light hover:bg-success/80',
72
- danger: 'bg-danger text-light hover:bg-danger/80',
73
- warning: 'bg-warning text-light hover:bg-warning/80',
74
- info: 'bg-info text-light hover:bg-info/80',
75
- },
58
+ filledColor: styleVariants.filledColor,
59
+ filledColorHover: styleVariants.filledColorHover,
76
60
  },
77
61
  });
78
62
 
@@ -82,6 +66,19 @@
82
66
  const descriptionId = $derived(description ? `description-${id}` : undefined);
83
67
  </script>
84
68
 
69
+ {#snippet icon(icon: string)}
70
+ <Icon
71
+ {icon}
72
+ size="100%"
73
+ class={cleanClass(
74
+ iconStyles({
75
+ filledColor: color,
76
+ filledColorHover: color,
77
+ }),
78
+ )}
79
+ />
80
+ {/snippet}
81
+
85
82
  <div class="flex flex-col gap-1">
86
83
  {#if label}
87
84
  <Label id={labelId} for={inputId} {label} {...labelProps} />
@@ -95,7 +92,7 @@
95
92
  class={cleanClass(
96
93
  containerStyles({
97
94
  size,
98
- color: invalid ? 'danger' : color,
95
+ borderColor: invalid ? 'danger' : color,
99
96
  shape,
100
97
  roundedSize: shape === 'semi-round' ? size : undefined,
101
98
  }),
@@ -111,9 +108,9 @@
111
108
  {#snippet children({ checked, indeterminate })}
112
109
  <div class={cleanClass('flex items-center justify-center text-current')}>
113
110
  {#if indeterminate}
114
- <Icon icon={mdiMinus} size="100%" class={cleanClass(iconStyles({ color }))} />
111
+ {@render icon(mdiMinus)}
115
112
  {:else if checked}
116
- <Icon icon={mdiCheck} size="100%" class={cleanClass(iconStyles({ color }))} />
113
+ {@render icon(mdiCheck)}
117
114
  {/if}
118
115
  </div>
119
116
  {/snippet}
@@ -4,7 +4,13 @@
4
4
  import type { CloseButtonProps } from '../../types.js';
5
5
  import { mdiClose } from '@mdi/js';
6
6
 
7
- const { size = 'medium', variant = 'ghost', translations, ...restProps }: CloseButtonProps = $props();
7
+ const {
8
+ size = 'medium',
9
+ variant = 'ghost',
10
+ color = 'secondary',
11
+ translations,
12
+ ...restProps
13
+ }: CloseButtonProps = $props();
8
14
  </script>
9
15
 
10
16
  <IconButton
@@ -13,6 +19,6 @@
13
19
  shape="round"
14
20
  {variant}
15
21
  {size}
16
- color="secondary"
22
+ {color}
17
23
  aria-label={t('close', translations)}
18
24
  />
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { styleVariants } from '../../styles.js';
2
3
  import type { Size, TextColor } from '../../types.js';
3
4
  import { cleanClass } from '../../utilities/internal.js';
4
5
  import type { Snippet } from 'svelte';
@@ -58,13 +59,7 @@
58
59
  info: 'border-info text-info border',
59
60
  },
60
61
 
61
- size: {
62
- tiny: 'text-xs',
63
- small: 'text-sm',
64
- medium: 'text-base',
65
- large: 'text-lg',
66
- giant: 'text-xl',
67
- },
62
+ size: styleVariants.textSize,
68
63
  },
69
64
  });
70
65
  </script>
@@ -3,6 +3,7 @@
3
3
  import Icon from '../Icon/Icon.svelte';
4
4
  import Label from '../Label/Label.svelte';
5
5
  import Text from '../Text/Text.svelte';
6
+ import { styleVariants } from '../../styles.js';
6
7
  import type { InputProps } from '../../types.js';
7
8
  import { cleanClass, generateId, isIconLike } from '../../utilities/internal.js';
8
9
  import { tv } from 'tailwind-variants';
@@ -39,11 +40,7 @@
39
40
  const containerStyles = tv({
40
41
  base: 'flex w-full items-center bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
41
42
  variants: {
42
- shape: {
43
- rectangle: 'rounded-none',
44
- 'semi-round': '',
45
- round: 'rounded-full',
46
- },
43
+ shape: styleVariants.shape,
47
44
  roundedSize: {
48
45
  tiny: 'rounded-xl',
49
46
  small: 'rounded-xl',
@@ -61,13 +58,7 @@
61
58
  const inputStyles = tv({
62
59
  base: 'flex-1 bg-transparent py-3 outline-none disabled:cursor-not-allowed',
63
60
  variants: {
64
- textSize: {
65
- tiny: 'text-xs',
66
- small: 'text-sm',
67
- medium: 'text-base',
68
- large: 'text-lg',
69
- giant: 'text-xl',
70
- },
61
+ textSize: styleVariants.textSize,
71
62
  leadingPadding: {
72
63
  base: 'pl-4',
73
64
  icon: 'pl-0',
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { styleVariants } from '../../styles.js';
2
3
  import type { Color, Size } from '../../types.js';
3
4
  import { cleanClass } from '../../utilities/internal.js';
4
5
  import { tv } from 'tailwind-variants';
@@ -21,14 +22,7 @@
21
22
  large: 'h-6',
22
23
  giant: 'h-12',
23
24
  },
24
- color: {
25
- primary: 'fill-primary',
26
- secondary: 'fill-dark',
27
- success: 'fill-success',
28
- danger: 'fill-danger',
29
- warning: 'fill-warning',
30
- info: 'fill-info',
31
- },
25
+ color: styleVariants.fillColor,
32
26
  },
33
27
  });
34
28
  </script>
@@ -5,7 +5,13 @@
5
5
  let { value = $bindable(), color = 'secondary', size, ...props }: NumberInputProps = $props();
6
6
 
7
7
  const getValue = () => (typeof value === 'number' ? String(value) : '');
8
- const setValue = (newValue: string) => {
8
+ const setValue = (newValue: string | number | null) => {
9
+ if (typeof newValue === 'number') {
10
+ value = newValue;
11
+ return;
12
+ }
13
+
14
+ // empty string or null
9
15
  if (!newValue) {
10
16
  value = undefined;
11
17
  return;
@@ -38,7 +38,7 @@
38
38
  });
39
39
 
40
40
  const bar = tv({
41
- base: 'h-8 w-13 rounded-full border border-2',
41
+ base: 'h-8 w-13 rounded-full border-2',
42
42
  variants: {
43
43
  fillColor: {
44
44
  default: 'border-gray-400 bg-gray-300 dark:border-gray-500 dark:bg-gray-400',
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Text from '../../internal/Text.svelte';
3
+ import { styleVariants } from '../../styles.js';
3
4
  import type { FontWeight, Size, TextColor, TextVariant } from '../../types.js';
4
5
  import { cleanClass } from '../../utilities/internal.js';
5
6
  import type { Snippet } from 'svelte';
@@ -19,13 +20,7 @@
19
20
 
20
21
  const styles = tv({
21
22
  variants: {
22
- size: {
23
- tiny: 'text-xs',
24
- small: 'text-sm',
25
- medium: 'text-base',
26
- large: 'text-lg',
27
- giant: 'text-xl',
28
- },
23
+ size: styleVariants.textSize,
29
24
  },
30
25
  });
31
26
  </script>
@@ -2,6 +2,7 @@
2
2
  import { getFieldContext } from '../../common/context.svelte.js';
3
3
  import Label from '../Label/Label.svelte';
4
4
  import Text from '../Text/Text.svelte';
5
+ import { styleVariants } from '../../styles.js';
5
6
  import type { TextareaProps } from '../../types.js';
6
7
  import { cleanClass, generateId } from '../../utilities/internal.js';
7
8
  import type { FormEventHandler } from 'svelte/elements';
@@ -23,11 +24,7 @@
23
24
  const styles = tv({
24
25
  base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
25
26
  variants: {
26
- shape: {
27
- rectangle: 'rounded-none',
28
- 'semi-round': '',
29
- round: 'rounded-full',
30
- },
27
+ shape: styleVariants.shape,
31
28
  padding: {
32
29
  base: 'px-4 py-3',
33
30
  round: 'px-5 py-3',
@@ -43,13 +40,7 @@
43
40
  large: 'rounded-2xl',
44
41
  giant: 'rounded-2xl',
45
42
  },
46
- textSize: {
47
- tiny: 'text-xs',
48
- small: 'text-sm',
49
- medium: 'text-base',
50
- large: 'text-lg',
51
- giant: 'text-xl',
52
- },
43
+ textSize: styleVariants.textSize,
53
44
  invalid: {
54
45
  true: 'border-danger/80 border',
55
46
  false: '',
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import ToastContainer from './ToastContainer.svelte';
3
+ import ToastContent from './ToastContent.svelte';
4
+ import type { ToastProps } from '../../types.js';
5
+
6
+ let { children, title, description, icon, onClose, ...props }: ToastProps = $props();
7
+ </script>
8
+
9
+ <ToastContainer {...props}>
10
+ {#if children}
11
+ {@render children()}
12
+ {:else if title}
13
+ <ToastContent {title} {description} {icon} {onClose} {...props} />
14
+ {/if}
15
+ </ToastContainer>
@@ -0,0 +1,4 @@
1
+ import type { ToastProps } from '../../types.js';
2
+ declare const Toast: import("svelte").Component<ToastProps, {}, "">;
3
+ type Toast = ReturnType<typeof Toast>;
4
+ export default Toast;
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import { styleVariants } from '../../styles.js';
3
+ import type { ToastContainerProps } from '../../types.js';
4
+ import { cleanClass } from '../../utilities/internal.js';
5
+ import { fade, fly } from 'svelte/transition';
6
+ import { tv } from 'tailwind-variants';
7
+
8
+ let {
9
+ color = 'primary',
10
+ shape = 'semi-round',
11
+ size = 'medium',
12
+ variant = 'filled',
13
+ class: className,
14
+ children,
15
+ ...restProps
16
+ }: ToastContainerProps = $props();
17
+
18
+ const containerStyles = tv({
19
+ base: 'bg-light text-dark overflow-hidden transition-all',
20
+ variants: {
21
+ shape: styleVariants.shape,
22
+ size: {
23
+ tiny: 'w-64',
24
+ small: 'w-72',
25
+ medium: 'w-xs',
26
+ large: 'w-sm',
27
+ giant: 'w-lg',
28
+ full: 'w-full',
29
+ },
30
+
31
+ border: styleVariants.border,
32
+ borderColor: styleVariants.borderColor,
33
+ roundedSize: {
34
+ tiny: 'rounded-lg',
35
+ small: 'rounded-lg',
36
+ medium: 'rounded-xl',
37
+ large: 'rounded-xl',
38
+ giant: 'rounded-2xl',
39
+ },
40
+ },
41
+ });
42
+
43
+ const container2Styles = tv({
44
+ variants: {
45
+ filled: {
46
+ primary: 'bg-primary/20 dark:bg-primary/25',
47
+ secondary: 'bg-dark/20 dark:bg-dark/25',
48
+ muted: 'bg-subtle dark:bg-subtle',
49
+ info: 'bg-info/20 dark:bg-info/25',
50
+ warning: 'bg-warning/20 dark:bg-warning/25',
51
+ danger: 'bg-danger/20 dark:bg-danger/25',
52
+ success: 'bg-success/20 dark:bg-success/25',
53
+ },
54
+ outline: {
55
+ primary: 'bg-primary/10 text-primary hover:bg-primary/20',
56
+ secondary: 'bg-dark/10 text-dark hover:bg-dark/20',
57
+ success: 'bg-success/10 text-success hover:bg-success/20',
58
+ danger: 'bg-danger/10 text-danger hover:bg-danger/20',
59
+ warning: 'bg-warning/10 text-warning hover:bg-warning/20',
60
+ info: 'bg-info/10 text-info hover:bg-info/20',
61
+ },
62
+ },
63
+ });
64
+ </script>
65
+
66
+ <div
67
+ out:fade|global={{ duration: 100 }}
68
+ in:fly|global={{ y: 200, duration: 250 }}
69
+ class={cleanClass(
70
+ containerStyles({
71
+ shape,
72
+ size,
73
+ roundedSize: shape === 'semi-round' ? 'medium' : undefined,
74
+ border: variant === 'outline',
75
+ borderColor: variant === 'outline' ? color : undefined,
76
+ }),
77
+ className,
78
+ )}
79
+ {...restProps}
80
+ >
81
+ <div
82
+ class={container2Styles({
83
+ outline: variant === 'outline' ? color : undefined,
84
+ filled: variant === 'filled' ? color : undefined,
85
+ })}
86
+ >
87
+ {@render children?.()}
88
+ </div>
89
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { ToastContainerProps } from '../../types.js';
2
+ declare const ToastContainer: import("svelte").Component<ToastContainerProps, {}, "">;
3
+ type ToastContainer = ReturnType<typeof ToastContainer>;
4
+ export default ToastContainer;
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import CloseButton from '../CloseButton/CloseButton.svelte';
3
+ import Icon from '../Icon/Icon.svelte';
4
+ import Text from '../Text/Text.svelte';
5
+ import { styleVariants } from '../../styles.js';
6
+ import type { ToastContentProps } from '../../types.js';
7
+ import { resolveIcon } from '../../utilities/internal.js';
8
+ import { mdiAlert, mdiBell, mdiCheck, mdiCloseOctagon, mdiInformation } from '@mdi/js';
9
+ import { type Snippet } from 'svelte';
10
+ import { tv } from 'tailwind-variants';
11
+
12
+ let {
13
+ color = 'primary',
14
+ variant = 'filled',
15
+ icon: iconOverride,
16
+ title,
17
+ description,
18
+ onClose,
19
+ children,
20
+ }: ToastContentProps = $props();
21
+
22
+ const icon = $derived(
23
+ resolveIcon({
24
+ icons: {
25
+ primary: mdiBell,
26
+ danger: mdiCloseOctagon,
27
+ info: mdiInformation,
28
+ warning: mdiAlert,
29
+ success: mdiCheck,
30
+ },
31
+ color,
32
+ override: iconOverride,
33
+ fallback: mdiBell,
34
+ }),
35
+ );
36
+
37
+ const iconStyles = tv({
38
+ base: 'h-10 w-10 shrink-0 py-2',
39
+ variants: {
40
+ color: styleVariants.textColor,
41
+ },
42
+ });
43
+ </script>
44
+
45
+ {#snippet resolve(text: string | Snippet)}
46
+ {#if typeof text === 'string'}{text}{:else}{@render text()}{/if}
47
+ {/snippet}
48
+
49
+ <div class="flex items-center px-2">
50
+ <div class="flex items-center">
51
+ {#if icon}
52
+ <Icon {icon} class={iconStyles({ color: variant === 'filled' ? color : undefined })} />
53
+ {/if}
54
+ </div>
55
+ <div class="flex grow justify-between">
56
+ <div class="flex flex-col p-2">
57
+ {#if title}
58
+ <Text fontWeight="bold">{@render resolve(title)}</Text>
59
+ {/if}
60
+ {#if description}
61
+ <Text size="small">{@render resolve(description)}</Text>
62
+ {/if}
63
+ </div>
64
+ {#if onClose}
65
+ <div class="flex items-center">
66
+ <CloseButton color={variant === 'filled' ? 'secondary' : color} variant="ghost" onclick={onClose} />
67
+ </div>
68
+ {/if}
69
+ </div>
70
+ </div>
71
+ {@render children?.()}
@@ -0,0 +1,4 @@
1
+ import type { ToastContentProps } from '../../types.js';
2
+ declare const ToastContent: import("svelte").Component<ToastContentProps, {}, "">;
3
+ type ToastContent = ReturnType<typeof ToastContent>;
4
+ export default ToastContent;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import Toast from './Toast.svelte';
3
+ import { zIndex } from '../../constants.js';
4
+ import { isCustomToast } from '../../services/toast-manager.svelte.js';
5
+ import type { ToastId, ToastItem } from '../../types.js';
6
+
7
+ type Props = {
8
+ items: Array<ToastItem & ToastId>;
9
+ };
10
+
11
+ const { items }: Props = $props();
12
+ </script>
13
+
14
+ <div class="absolute top-0 right-0 flex flex-col items-end justify-end gap-2 p-4 {zIndex.ToastPanel}">
15
+ {#each items as item (item.id)}
16
+ {#if isCustomToast(item)}
17
+ <item.component {...item.props} />
18
+ {:else}
19
+ <Toast {...item} />
20
+ {/if}
21
+ {/each}
22
+ </div>
@@ -0,0 +1,7 @@
1
+ import type { ToastId, ToastItem } from '../../types.js';
2
+ type Props = {
3
+ items: Array<ToastItem & ToastId>;
4
+ };
5
+ declare const ToastPanel: import("svelte").Component<Props, {}, "">;
6
+ type ToastPanel = ReturnType<typeof ToastPanel>;
7
+ export default ToastPanel;
@@ -17,4 +17,5 @@ export declare const zIndex: {
17
17
  AppShellSidebar: string;
18
18
  ModalBackdrop: string;
19
19
  ModalContent: string;
20
+ ToastPanel: string;
20
21
  };
package/dist/constants.js CHANGED
@@ -18,4 +18,5 @@ export const zIndex = {
18
18
  AppShellSidebar: 'z-30',
19
19
  ModalBackdrop: 'z-40',
20
20
  ModalContent: 'z-50',
21
+ ToastPanel: 'z-60',
21
22
  };
package/dist/index.d.ts CHANGED
@@ -62,9 +62,14 @@ export { default as Switch } from './components/Switch/Switch.svelte';
62
62
  export { default as Text } from './components/Text/Text.svelte';
63
63
  export { default as Textarea } from './components/Textarea/Textarea.svelte';
64
64
  export { default as ThemeSwitcher } from './components/ThemeSwitcher/ThemeSwitcher.svelte';
65
+ export { default as Toast } from './components/Toast/Toast.svelte';
66
+ export { default as ToastContainer } from './components/Toast/ToastContainer.svelte';
67
+ export { default as ToastContent } from './components/Toast/ToastContent.svelte';
68
+ export { default as ToastPanel } from './components/Toast/ToastPanel.svelte';
65
69
  export * from './services/command-palette-manager.svelte.js';
66
70
  export * from './services/modal-manager.svelte.js';
67
71
  export * from './services/theme.svelte.js';
72
+ export * from './services/toast-manager.svelte.js';
68
73
  export * from './services/translation.svelte.js';
69
74
  export * from './types.js';
70
75
  export * from './utilities/byte-units.js';
package/dist/index.js CHANGED
@@ -64,10 +64,15 @@ export { default as Switch } from './components/Switch/Switch.svelte';
64
64
  export { default as Text } from './components/Text/Text.svelte';
65
65
  export { default as Textarea } from './components/Textarea/Textarea.svelte';
66
66
  export { default as ThemeSwitcher } from './components/ThemeSwitcher/ThemeSwitcher.svelte';
67
+ export { default as Toast } from './components/Toast/Toast.svelte';
68
+ export { default as ToastContainer } from './components/Toast/ToastContainer.svelte';
69
+ export { default as ToastContent } from './components/Toast/ToastContent.svelte';
70
+ export { default as ToastPanel } from './components/Toast/ToastPanel.svelte';
67
71
  // helpers
68
72
  export * from './services/command-palette-manager.svelte.js';
69
73
  export * from './services/modal-manager.svelte.js';
70
74
  export * from './services/theme.svelte.js';
75
+ export * from './services/toast-manager.svelte.js';
71
76
  export * from './services/translation.svelte.js';
72
77
  export * from './types.js';
73
78
  export * from './utilities/byte-units.js';
@@ -42,11 +42,7 @@
42
42
  true: 'disabled:pointer-events-none disabled:opacity-50 aria-disabled:opacity-50',
43
43
  false: 'cursor-pointer',
44
44
  },
45
- shape: {
46
- rectangle: 'rounded-none',
47
- 'semi-round': 'rounded-xl',
48
- round: 'rounded-full',
49
- },
45
+ shape: styleVariants.shape,
50
46
  fullWidth: {
51
47
  true: 'w-full',
52
48
  },
@@ -72,14 +68,8 @@
72
68
  large: 'rounded-xl',
73
69
  giant: 'rounded-2xl',
74
70
  },
75
- filledColor: {
76
- primary: 'bg-primary text-light hover:bg-primary/80',
77
- secondary: 'bg-dark text-light hover:bg-dark/80',
78
- success: 'bg-success text-light hover:bg-success/80',
79
- danger: 'bg-danger text-light hover:bg-danger/80',
80
- warning: 'bg-warning text-light hover:bg-warning/80',
81
- info: 'bg-info text-light hover:bg-info/80',
82
- },
71
+ filledColor: styleVariants.filledColor,
72
+ filledColorHover: styleVariants.filledColorHover,
83
73
  outlineColor: {
84
74
  primary: 'border-primary bg-primary/10 text-primary hover:bg-primary/20 border',
85
75
  secondary: 'border-dark bg-dark/10 text-dark hover:bg-dark/20 border',
@@ -119,6 +109,7 @@
119
109
  disabled,
120
110
  roundedSize: shape === 'semi-round' ? size : undefined,
121
111
  filledColor: variant === 'filled' ? color : undefined,
112
+ filledColorHover: variant === 'filled' ? color : undefined,
122
113
  outlineColor: variant === 'outline' ? color : undefined,
123
114
  ghostColor: variant === 'ghost' ? color : undefined,
124
115
  }),
@@ -148,6 +139,19 @@
148
139
  {/if}
149
140
  {/snippet}
150
141
 
142
+ {#snippet wrapper()}
143
+ {#if loading}
144
+ <div class="flex items-center justify-center gap-2">
145
+ <LoadingSpinner {color} size={spinnerSizes[size]} />
146
+ {#if !icon}
147
+ {@render content()}
148
+ {/if}
149
+ </div>
150
+ {:else}
151
+ {@render content()}
152
+ {/if}
153
+ {/snippet}
154
+
151
155
  {#if href}
152
156
  {@const resolved = resolveUrl(href)}
153
157
  {@const external = isExternalLink(resolved)}
@@ -160,14 +164,7 @@
160
164
  rel={external ? 'noopener noreferrer' : undefined}
161
165
  {...restProps as HTMLAnchorAttributes}
162
166
  >
163
- {#if loading}
164
- <div class="flex items-center justify-center gap-2">
165
- <LoadingSpinner {color} size={spinnerSizes[size]} />
166
- {@render content()}
167
- </div>
168
- {:else}
169
- {@render content()}
170
- {/if}
167
+ {@render wrapper()}
171
168
  </a>
172
169
  {:else}
173
170
  <ButtonPrimitive.Root
@@ -178,13 +175,6 @@
178
175
  {disabled}
179
176
  aria-disabled={disabled}
180
177
  >
181
- {#if loading}
182
- <div class="flex items-center justify-center gap-2">
183
- <LoadingSpinner {color} size={spinnerSizes[size]} />
184
- {@render content()}
185
- </div>
186
- {:else}
187
- {@render content()}
188
- {/if}
178
+ {@render wrapper()}
189
179
  </ButtonPrimitive.Root>
190
180
  {/if}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { styleVariants } from '../styles.js';
2
3
  import type { FontWeight, HeadingColor, HeadingTag, Size, TextVariant } from '../types.js';
3
4
  import { cleanClass } from '../utilities/internal.js';
4
5
  import type { Snippet } from 'svelte';
@@ -32,20 +33,8 @@
32
33
  warning: 'text-warning',
33
34
  info: 'text-info',
34
35
  },
35
- fontWeight: {
36
- light: 'font-light',
37
- normal: 'font-normal',
38
- 'semi-bold': 'font-semibold',
39
- bold: 'font-bold',
40
- 'extra-bold': 'font-extrabold',
41
- },
42
- size: {
43
- tiny: 'text-xs',
44
- small: 'text-sm',
45
- medium: 'text-base',
46
- large: 'text-lg',
47
- giant: 'text-xl',
48
- },
36
+ fontWeight: styleVariants.fontWeight,
37
+ size: styleVariants.textSize,
49
38
  variant: {
50
39
  italic: 'italic',
51
40
  },
@@ -0,0 +1,18 @@
1
+ import type { ToastCustom, ToastItem, ToastOptions, ToastShow } from '../types.js';
2
+ export declare const isCustomToast: (item: ToastItem) => item is ToastCustom;
3
+ declare class ToastManager {
4
+ #private;
5
+ show(item: ToastShow, options?: ToastOptions): void;
6
+ custom(item: ToastCustom, options?: ToastOptions): void;
7
+ open(item: ToastItem, options?: ToastOptions): void;
8
+ unmount(): Promise<void>;
9
+ primary(item: ToastShow): void;
10
+ success(item: ToastShow): void;
11
+ info(item: ToastShow): void;
12
+ warning(item: ToastShow): void;
13
+ danger(item: ToastShow): void;
14
+ mount(): Promise<void>;
15
+ private remove;
16
+ }
17
+ export declare const toastManager: ToastManager;
18
+ export {};
@@ -0,0 +1,65 @@
1
+ import ToastPanel from '../components/Toast/ToastPanel.svelte';
2
+ import { generateId } from '../utilities/internal.js';
3
+ import { mount, unmount } from 'svelte';
4
+ export const isCustomToast = (item) => !!item.component;
5
+ class ToastManager {
6
+ #ref;
7
+ #props = $state({ items: [] });
8
+ show(item, options) {
9
+ return this.open(item, options);
10
+ }
11
+ custom(item, options) {
12
+ return this.open(item, options);
13
+ }
14
+ open(item, options) {
15
+ const { timeout = 3000, closable = true, id = generateId() } = options || {};
16
+ const toast = item;
17
+ toast.id = id;
18
+ if (closable) {
19
+ const onClose = () => this.remove(toast);
20
+ if (isCustomToast(item)) {
21
+ item.props.onClose = onClose;
22
+ }
23
+ else {
24
+ item.onClose = onClose;
25
+ }
26
+ }
27
+ this.#props.items.push(toast);
28
+ void this.mount();
29
+ if (timeout) {
30
+ setTimeout(() => this.remove(toast), timeout);
31
+ }
32
+ }
33
+ async unmount() {
34
+ if (this.#ref) {
35
+ await unmount(this.#ref);
36
+ }
37
+ }
38
+ primary(item) {
39
+ this.show({ ...item, color: 'primary' });
40
+ }
41
+ success(item) {
42
+ this.show({ ...item, color: 'success' });
43
+ }
44
+ info(item) {
45
+ this.show({ ...item, color: 'info' });
46
+ }
47
+ warning(item) {
48
+ this.show({ ...item, color: 'warning' });
49
+ }
50
+ danger(item) {
51
+ this.show({ ...item, color: 'danger' });
52
+ }
53
+ async mount() {
54
+ if (!this.#ref) {
55
+ this.#ref = await mount(ToastPanel, {
56
+ target: document.body,
57
+ props: this.#props,
58
+ });
59
+ }
60
+ }
61
+ remove(target) {
62
+ this.#props.items = this.#props.items.filter((item) => item.id !== target.id);
63
+ }
64
+ }
65
+ export const toastManager = new ToastManager();
package/dist/styles.d.ts CHANGED
@@ -16,6 +16,47 @@ export declare const styleVariants: {
16
16
  warning: string;
17
17
  info: string;
18
18
  };
19
+ shape: {
20
+ rectangle: string;
21
+ 'semi-round': string;
22
+ round: string;
23
+ };
24
+ border: {
25
+ true: string;
26
+ false: string;
27
+ };
28
+ borderColor: {
29
+ primary: string;
30
+ secondary: string;
31
+ success: string;
32
+ danger: string;
33
+ warning: string;
34
+ info: string;
35
+ };
36
+ fillColor: {
37
+ primary: string;
38
+ secondary: string;
39
+ success: string;
40
+ danger: string;
41
+ warning: string;
42
+ info: string;
43
+ };
44
+ filledColor: {
45
+ primary: string;
46
+ secondary: string;
47
+ success: string;
48
+ danger: string;
49
+ warning: string;
50
+ info: string;
51
+ };
52
+ filledColorHover: {
53
+ primary: string;
54
+ secondary: string;
55
+ success: string;
56
+ danger: string;
57
+ warning: string;
58
+ info: string;
59
+ };
19
60
  textSize: {
20
61
  tiny: string;
21
62
  small: string;
@@ -23,4 +64,11 @@ export declare const styleVariants: {
23
64
  large: string;
24
65
  giant: string;
25
66
  };
67
+ fontWeight: {
68
+ light: string;
69
+ normal: string;
70
+ 'semi-bold': string;
71
+ bold: string;
72
+ 'extra-bold': string;
73
+ };
26
74
  };
package/dist/styles.js CHANGED
@@ -12,6 +12,47 @@ export const styleVariants = {
12
12
  ...color,
13
13
  muted: 'text-gray-600 dark:text-gray-400',
14
14
  },
15
+ shape: {
16
+ rectangle: 'rounded-none',
17
+ 'semi-round': '',
18
+ round: 'rounded-full',
19
+ },
20
+ border: {
21
+ true: 'border',
22
+ false: '',
23
+ },
24
+ borderColor: {
25
+ primary: 'border-primary',
26
+ secondary: 'border-dark',
27
+ success: 'border-success',
28
+ danger: 'border-danger',
29
+ warning: 'border-warning',
30
+ info: 'border-info',
31
+ },
32
+ fillColor: {
33
+ primary: 'fill-primary',
34
+ secondary: 'fill-dark',
35
+ success: 'fill-success',
36
+ danger: 'fill-danger',
37
+ warning: 'fill-warning',
38
+ info: 'fill-info',
39
+ },
40
+ filledColor: {
41
+ primary: 'bg-primary text-light',
42
+ secondary: 'bg-dark text-light',
43
+ success: 'bg-success text-light',
44
+ danger: 'bg-danger text-light',
45
+ warning: 'bg-warning text-light',
46
+ info: 'bg-info text-light',
47
+ },
48
+ filledColorHover: {
49
+ primary: 'hover:bg-primary/80',
50
+ secondary: 'hover:bg-dark/80',
51
+ success: 'hover:bg-success/80',
52
+ danger: 'hover:bg-danger/80',
53
+ warning: 'hover:bg-warning/80',
54
+ info: 'hover:bg-info/80',
55
+ },
15
56
  textSize: {
16
57
  tiny: 'text-xs',
17
58
  small: 'text-sm',
@@ -19,4 +60,11 @@ export const styleVariants = {
19
60
  large: 'text-lg',
20
61
  giant: 'text-xl',
21
62
  },
63
+ fontWeight: {
64
+ light: 'font-light',
65
+ normal: 'font-normal',
66
+ 'semi-bold': 'font-semibold',
67
+ bold: 'font-bold',
68
+ 'extra-bold': 'font-extrabold',
69
+ },
22
70
  };
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Translations } from './services/translation.svelte.js';
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAnchorAttributes, HTMLButtonAttributes, HTMLInputAttributes, HTMLLabelAttributes, HTMLTextareaAttributes } from 'svelte/elements';
2
+ import type { Component, Snippet } from 'svelte';
3
+ import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes, HTMLInputAttributes, HTMLLabelAttributes, HTMLTextareaAttributes } from 'svelte/elements';
4
4
  export type Color = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
5
5
  export type TextColor = Color | 'muted';
6
6
  export type TextVariant = 'italic';
@@ -13,6 +13,7 @@ export type HeadingSize = Size | 'title';
13
13
  export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
14
14
  export type Shape = 'rectangle' | 'semi-round' | 'round';
15
15
  export type Variants = 'filled' | 'outline' | 'ghost';
16
+ export type ToastVariant = 'filled' | 'outline';
16
17
  export type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
17
18
  export declare enum Theme {
18
19
  Light = "light",
@@ -40,25 +41,26 @@ export type IconProps = {
40
41
  };
41
42
  type ButtonOrAnchor = ({
42
43
  href?: never;
43
- } & HTMLButtonAttributes) | ({
44
+ } & Omit<HTMLButtonAttributes, 'color' | 'size'>) | ({
44
45
  href: string;
45
- } & HTMLAnchorAttributes);
46
+ } & Omit<HTMLAnchorAttributes, 'color' | 'size'>);
46
47
  type ButtonBase = {
47
48
  size?: Size;
48
49
  variant?: Variants;
49
50
  class?: string;
50
51
  color?: Color;
51
52
  shape?: Shape;
53
+ loading?: boolean;
52
54
  };
53
55
  export type ButtonProps = ButtonBase & {
54
56
  ref?: HTMLElement | null;
55
57
  fullWidth?: boolean;
56
- loading?: boolean;
57
58
  leadingIcon?: IconLike;
58
59
  trailingIcon?: IconLike;
59
60
  } & ButtonOrAnchor;
60
61
  export type CloseButtonProps = {
61
62
  size?: Size;
63
+ color?: Color;
62
64
  variant?: Variants;
63
65
  class?: string;
64
66
  translations?: TranslationProps<'close'>;
@@ -151,4 +153,52 @@ export type MultiSelectProps<T extends SelectItem> = SelectCommonProps<T> & {
151
153
  values?: T[];
152
154
  onChange?: (values: T[]) => void;
153
155
  };
156
+ export type ToastId = {
157
+ id: string;
158
+ };
159
+ type ToastCommonProps = {
160
+ color?: Color;
161
+ variant?: ToastVariant;
162
+ };
163
+ export type ToastContentProps = ToastCommonProps & {
164
+ title?: string | Snippet;
165
+ description?: string | Snippet;
166
+ icon?: IconLike | false;
167
+ onClose?: () => void;
168
+ children?: Snippet;
169
+ };
170
+ export type ToastContainerProps = ToastCommonProps & {
171
+ shape?: Shape;
172
+ size?: ContainerSize;
173
+ } & Omit<HTMLAttributes<HTMLElement>, 'title' | 'color' | 'size'>;
174
+ export type ToastProps = ToastContentProps & ToastContainerProps;
175
+ type Closable = {
176
+ onClose: () => void;
177
+ };
178
+ export type ToastCustom<T extends Closable = any> = {
179
+ component: Component<T>;
180
+ props: T;
181
+ };
182
+ export type ToastShow = {
183
+ title: string;
184
+ description?: string;
185
+ color?: Color;
186
+ shape?: Shape;
187
+ size?: ContainerSize;
188
+ variant?: ToastVariant;
189
+ };
190
+ export type ToastOptions = {
191
+ id?: string;
192
+ timeout?: number;
193
+ closable?: boolean;
194
+ };
195
+ export type ToastItem = ToastProps | ToastCustom;
196
+ export type ToastButton = {
197
+ label: string;
198
+ size?: Size;
199
+ color?: Color;
200
+ shape?: Shape;
201
+ variant?: Variants;
202
+ onClick: () => void;
203
+ };
154
204
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.37.1",
3
+ "version": "0.38.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "repository": {
6
6
  "type": "git",