@immich/ui 0.24.5 → 0.25.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.
@@ -2,7 +2,7 @@
2
2
  import Button from '../../internal/Button.svelte';
3
3
  import type { ButtonProps } from '../../types.js';
4
4
 
5
- const props: ButtonProps = $props();
5
+ let { ref = $bindable(null), ...props }: ButtonProps = $props();
6
6
  </script>
7
7
 
8
- <Button {...props} />
8
+ <Button bind:ref {...props} />
@@ -1,5 +1,5 @@
1
1
  import Button from '../../internal/Button.svelte';
2
2
  import type { ButtonProps } from '../../types.js';
3
- declare const Button: import("svelte").Component<ButtonProps, {}, "">;
3
+ declare const Button: import("svelte").Component<ButtonProps, {}, "ref">;
4
4
  type Button = ReturnType<typeof Button>;
5
5
  export default Button;
@@ -12,6 +12,7 @@
12
12
  import { tv } from 'tailwind-variants';
13
13
 
14
14
  type Props = HTMLAttributes<HTMLDivElement> & {
15
+ ref?: HTMLElement | null;
15
16
  color?: Color;
16
17
  shape?: 'round' | 'rectangle';
17
18
  expanded?: boolean;
@@ -20,6 +21,7 @@
20
21
  };
21
22
 
22
23
  let {
24
+ ref = $bindable(null),
23
25
  color,
24
26
  class: className,
25
27
  shape = 'round',
@@ -125,7 +127,11 @@
125
127
  {/if}
126
128
  {/snippet}
127
129
 
128
- <div class={cleanClass(containerStyles({ shape, border: !color }), className)} {...restProps}>
130
+ <div
131
+ bind:this={ref}
132
+ class={cleanClass(containerStyles({ shape, border: !color }), className)}
133
+ {...restProps}
134
+ >
129
135
  <div class={cleanClass(cardStyles({ color }))}>
130
136
  {#if headerChild}
131
137
  {@render header()}
@@ -2,12 +2,13 @@ import type { Color } from '../../types.js';
2
2
  import { type Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  type Props = HTMLAttributes<HTMLDivElement> & {
5
+ ref?: HTMLElement | null;
5
6
  color?: Color;
6
7
  shape?: 'round' | 'rectangle';
7
8
  expanded?: boolean;
8
9
  expandable?: boolean;
9
10
  children: Snippet;
10
11
  };
11
- declare const Card: import("svelte").Component<Props, {}, "expanded">;
12
+ declare const Card: import("svelte").Component<Props, {}, "ref" | "expanded">;
12
13
  type Card = ReturnType<typeof Card>;
13
14
  export default Card;
@@ -16,21 +16,25 @@
16
16
  import { tv } from 'tailwind-variants';
17
17
 
18
18
  type Props = {
19
- title: string;
19
+ title?: string;
20
+ icon?: string | boolean;
20
21
  size?: ModalSize;
21
22
  class?: string;
22
- icon?: string | boolean;
23
23
  expandable?: boolean;
24
+ closeOnEsc?: boolean;
25
+ closeOnBackdropClick?: boolean;
24
26
  children: Snippet;
25
27
  onClose?: () => void;
26
28
  };
27
29
 
28
30
  let {
29
- title,
30
31
  size = 'medium',
31
- icon = true,
32
32
  onClose,
33
+ icon = true,
34
+ title,
33
35
  class: className,
36
+ closeOnEsc = true,
37
+ closeOnBackdropClick = false,
34
38
  children,
35
39
  }: Props = $props();
36
40
 
@@ -49,6 +53,7 @@
49
53
  });
50
54
 
51
55
  const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Modal);
56
+ const headerChildren = $derived(getChildSnippet(ChildKey.ModalHeader));
52
57
  const bodyChildren = $derived(getChildSnippet(ChildKey.ModalBody));
53
58
  const footerChildren = $derived(getChildSnippet(ChildKey.ModalFooter));
54
59
 
@@ -59,6 +64,16 @@
59
64
 
60
65
  onClose?.();
61
66
  };
67
+
68
+ let cardRef = $state<HTMLElement | null>(null);
69
+
70
+ const handleCloseOnClick = (event: Event) => {
71
+ if (!closeOnBackdropClick || cardRef?.contains(event.target as Node)) {
72
+ return;
73
+ }
74
+
75
+ onClose?.();
76
+ };
62
77
  </script>
63
78
 
64
79
  <Dialog.Root open={true}>
@@ -69,25 +84,34 @@
69
84
  if (e.key === 'Escape') {
70
85
  e.stopPropagation();
71
86
  e.preventDefault();
72
- handleClose();
87
+ if (closeOnEsc) {
88
+ handleClose();
89
+ }
73
90
  }
74
91
  }}
92
+ onclick={handleCloseOnClick}
75
93
  class={cleanClass(
76
94
  'fixed start-0 top-0 flex h-dvh w-screen items-center justify-center overflow-hidden sm:p-4',
77
95
  )}
78
96
  >
79
97
  <div class={cleanClass('flex h-full w-full flex-col items-center justify-center')}>
80
- <Card class={cleanClass(modalStyles({ size }), className)}>
98
+ <Card bind:ref={cardRef} class={cleanClass(modalStyles({ size }), className)}>
81
99
  <CardHeader class="border-b border-gray-200 px-5 py-3 dark:border-white/10">
82
- <div class="flex items-center justify-between gap-2">
83
- {#if typeof icon === 'string'}
84
- <Icon {icon} size="1.5rem" aria-hidden />
85
- {:else if icon}
86
- <Logo variant="icon" size="tiny" />
87
- {/if}
88
- <CardTitle tag="p" class="text-dark/90 grow text-lg font-semibold">{title}</CardTitle>
89
- <CloseButton class="-me-2" onclick={() => handleClose()} />
90
- </div>
100
+ {#if headerChildren}
101
+ {@render headerChildren.snippet()}
102
+ {:else if title}
103
+ <div class="flex items-center justify-between gap-2">
104
+ {#if typeof icon === 'string'}
105
+ <Icon {icon} size="1.5rem" aria-hidden />
106
+ {:else if icon}
107
+ <Logo variant="icon" size="tiny" />
108
+ {/if}
109
+ <CardTitle tag="p" class="text-dark/90 grow text-lg font-semibold"
110
+ >{title}</CardTitle
111
+ >
112
+ <CloseButton class="-me-2" onclick={() => handleClose()} />
113
+ </div>
114
+ {/if}
91
115
  </CardHeader>
92
116
 
93
117
  <CardBody class="grow px-5">
@@ -1,11 +1,13 @@
1
1
  import type { ModalSize } from '../../types.js';
2
2
  import { type Snippet } from 'svelte';
3
3
  type Props = {
4
- title: string;
4
+ title?: string;
5
+ icon?: string | boolean;
5
6
  size?: ModalSize;
6
7
  class?: string;
7
- icon?: string | boolean;
8
8
  expandable?: boolean;
9
+ closeOnEsc?: boolean;
10
+ closeOnBackdropClick?: boolean;
9
11
  children: Snippet;
10
12
  onClose?: () => void;
11
13
  };
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import IconButton from '../IconButton/IconButton.svelte';
3
- import { onThemeChange, theme } from '../../services/theme.svelte.js';
3
+ import { theme, toggleTheme } from '../../services/theme.svelte.js';
4
4
  import { t } from '../../services/translation.svelte.js';
5
5
  import {
6
6
  Theme,
@@ -31,9 +31,8 @@
31
31
  }: Props = $props();
32
32
 
33
33
  const handleToggleTheme = () => {
34
- theme.value = theme.value === Theme.Dark ? Theme.Light : Theme.Dark;
34
+ toggleTheme();
35
35
  onChange?.(theme.value);
36
- onThemeChange();
37
36
  };
38
37
 
39
38
  const themeIcon = $derived(theme.value === Theme.Light ? mdiWeatherSunny : mdiWeatherNight);
package/dist/index.d.ts CHANGED
@@ -39,6 +39,7 @@ export { default as Link } from './components/Link/Link.svelte';
39
39
  export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
40
40
  export { default as Logo } from './components/Logo/Logo.svelte';
41
41
  export { default as Modal } from './components/Modal/Modal.svelte';
42
+ export { default as ModalHeader } from './components/Modal/ModalHeader.svelte';
42
43
  export { default as ModalBody } from './components/Modal/ModalBody.svelte';
43
44
  export { default as ModalFooter } from './components/Modal/ModalFooter.svelte';
44
45
  export { default as MultiSelect } from './components/MultiSelect/MultiSelect.svelte';
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ export { default as Link } from './components/Link/Link.svelte';
41
41
  export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
42
42
  export { default as Logo } from './components/Logo/Logo.svelte';
43
43
  export { default as Modal } from './components/Modal/Modal.svelte';
44
+ export { default as ModalHeader } from './components/Modal/ModalHeader.svelte';
44
45
  export { default as ModalBody } from './components/Modal/ModalBody.svelte';
45
46
  export { default as ModalFooter } from './components/Modal/ModalFooter.svelte';
46
47
  export { default as MultiSelect } from './components/MultiSelect/MultiSelect.svelte';
@@ -13,7 +13,8 @@
13
13
  icon?: boolean;
14
14
  };
15
15
 
16
- const {
16
+ let {
17
+ ref = $bindable(null),
17
18
  type = 'button',
18
19
  href,
19
20
  variant = 'filled',
@@ -152,7 +153,13 @@
152
153
  {/snippet}
153
154
 
154
155
  {#if href}
155
- <a {href} class={classList} aria-disabled={disabled} {...restProps as HTMLAnchorAttributes}>
156
+ <a
157
+ bind:this={ref}
158
+ {href}
159
+ class={classList}
160
+ aria-disabled={disabled}
161
+ {...restProps as HTMLAnchorAttributes}
162
+ >
156
163
  {#if loading}
157
164
  <div class="flex items-center justify-center gap-2">
158
165
  <LoadingSpinner {color} size={spinnerSizes[size]} />
@@ -164,6 +171,7 @@
164
171
  </a>
165
172
  {:else}
166
173
  <ButtonPrimitive.Root
174
+ bind:ref
167
175
  class={classList}
168
176
  type={type as HTMLButtonAttributes['type']}
169
177
  {...restProps as HTMLButtonAttributes}
@@ -3,6 +3,6 @@ type InternalButtonProps = ButtonProps & {
3
3
  /** when true, button width to height ratio is 1:1 */
4
4
  icon?: boolean;
5
5
  };
6
- declare const Button: import("svelte").Component<InternalButtonProps, {}, "">;
6
+ declare const Button: import("svelte").Component<InternalButtonProps, {}, "ref">;
7
7
  type Button = ReturnType<typeof Button>;
8
8
  export default Button;
@@ -14,5 +14,6 @@ type ThemePreference = {
14
14
  };
15
15
  export declare const theme: ThemePreference;
16
16
  export declare const onThemeChange: () => void;
17
+ export declare const toggleTheme: () => void;
17
18
  export declare const initializeTheme: (options?: ThemeOptions) => void;
18
19
  export {};
@@ -56,6 +56,10 @@ const syncToDom = () => {
56
56
  }
57
57
  }
58
58
  };
59
+ export const toggleTheme = () => {
60
+ theme.value = theme.value === Theme.Dark ? Theme.Light : Theme.Dark;
61
+ onThemeChange();
62
+ };
59
63
  export const initializeTheme = (options) => {
60
64
  if (options) {
61
65
  setThemeOptions(options);
package/dist/types.d.ts CHANGED
@@ -49,6 +49,7 @@ type ButtonBase = {
49
49
  shape?: Shape;
50
50
  };
51
51
  export type ButtonProps = ButtonBase & {
52
+ ref?: HTMLElement | null;
52
53
  fullWidth?: boolean;
53
54
  loading?: boolean;
54
55
  leadingIcon?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.24.5",
3
+ "version": "0.25.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "scripts": {
6
6
  "create": "node scripts/create.js",