@immich/ui 0.7.0 → 0.9.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.
Files changed (64) hide show
  1. package/dist/assets/appstore-badge.svg +46 -0
  2. package/dist/assets/fdroid-badge.svg +124 -0
  3. package/dist/assets/playstore-badge.png +0 -0
  4. package/dist/components/Alert/Alert.svelte +73 -19
  5. package/dist/components/Alert/Alert.svelte.d.ts +7 -1
  6. package/dist/components/AppShell/AppShell.svelte +34 -0
  7. package/dist/components/AppShell/AppShell.svelte.d.ts +6 -0
  8. package/dist/components/AppShell/AppShellHeader.svelte +15 -0
  9. package/dist/components/AppShell/AppShellHeader.svelte.d.ts +5 -0
  10. package/dist/components/AppShell/AppShellSidebar.svelte +23 -0
  11. package/dist/components/AppShell/AppShellSidebar.svelte.d.ts +7 -0
  12. package/dist/components/AppShell/PageLayout.svelte +44 -0
  13. package/dist/components/AppShell/PageLayout.svelte.d.ts +9 -0
  14. package/dist/components/Avatar/Avatar.svelte +66 -0
  15. package/dist/components/Avatar/Avatar.svelte.d.ts +7 -0
  16. package/dist/components/Card/Card.svelte +9 -11
  17. package/dist/components/Card/CardBody.svelte +1 -1
  18. package/dist/components/Card/CardFooter.svelte +6 -2
  19. package/dist/components/Card/CardFooter.svelte.d.ts +1 -0
  20. package/dist/components/CloseButton/CloseButton.svelte +2 -2
  21. package/dist/components/Code/Code.svelte +62 -0
  22. package/dist/components/Code/Code.svelte.d.ts +9 -0
  23. package/dist/components/Form/Checkbox.svelte +67 -27
  24. package/dist/components/Form/Checkbox.svelte.d.ts +1 -1
  25. package/dist/components/Form/HelperText.svelte +3 -3
  26. package/dist/components/Form/HelperText.svelte.d.ts +2 -2
  27. package/dist/components/Form/Input.svelte +2 -1
  28. package/dist/components/Form/Input.svelte.d.ts +1 -1
  29. package/dist/components/Form/PasswordInput.svelte +3 -1
  30. package/dist/components/Form/PasswordInput.svelte.d.ts +1 -1
  31. package/dist/components/FormatBytes/FormatBytes.svelte +16 -0
  32. package/dist/components/FormatBytes/FormatBytes.svelte.d.ts +6 -0
  33. package/dist/components/Heading/Heading.svelte +3 -2
  34. package/dist/components/Heading/Heading.svelte.d.ts +2 -2
  35. package/dist/components/Logo/Logo.svelte +8 -8
  36. package/dist/components/Logo/Logo.svelte.d.ts +1 -2
  37. package/dist/components/MultiSelect/MultiSelect.svelte +15 -0
  38. package/dist/components/MultiSelect/MultiSelect.svelte.d.ts +3 -0
  39. package/dist/components/Navbar/NavbarGroup.svelte +12 -0
  40. package/dist/components/Navbar/NavbarGroup.svelte.d.ts +4 -0
  41. package/dist/components/Navbar/NavbarItem.svelte +30 -0
  42. package/dist/components/Navbar/NavbarItem.svelte.d.ts +7 -0
  43. package/dist/components/Scrollable/Scrollable.svelte +41 -0
  44. package/dist/components/Scrollable/Scrollable.svelte.d.ts +6 -0
  45. package/dist/components/Select/Select.svelte +15 -0
  46. package/dist/components/Select/Select.svelte.d.ts +3 -0
  47. package/dist/components/Switch/Switch.svelte +99 -0
  48. package/dist/components/Switch/Switch.svelte.d.ts +10 -0
  49. package/dist/components/Text/Text.svelte +16 -4
  50. package/dist/components/Text/Text.svelte.d.ts +2 -2
  51. package/dist/constants.d.ts +3 -0
  52. package/dist/constants.js +3 -0
  53. package/dist/index.d.ts +23 -0
  54. package/dist/index.js +26 -0
  55. package/dist/internal/Button.svelte +1 -10
  56. package/dist/internal/Select.svelte +174 -0
  57. package/dist/internal/Select.svelte.d.ts +9 -0
  58. package/dist/services/theme.svelte.d.ts +5 -0
  59. package/dist/services/theme.svelte.js +13 -0
  60. package/dist/types.d.ts +53 -18
  61. package/dist/types.js +5 -1
  62. package/dist/utilities/byte-units.d.ts +52 -0
  63. package/dist/utilities/byte-units.js +75 -0
  64. package/package.json +6 -4
@@ -3,7 +3,7 @@
3
3
  import type { CloseButtonProps } from '../../types.js';
4
4
  import { mdiClose } from '@mdi/js';
5
5
 
6
- const { size = 'medium', variant = 'ghost' }: CloseButtonProps = $props();
6
+ const { size = 'medium', variant = 'ghost', ...restProps }: CloseButtonProps = $props();
7
7
  </script>
8
8
 
9
- <IconButton icon={mdiClose} color="secondary" shape="round" {variant} {size} />
9
+ <IconButton {...restProps} icon={mdiClose} shape="round" {variant} {size} color="secondary" />
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { Size, TextColor } from '../../types.js';
3
+ import type { Snippet } from 'svelte';
4
+ import { tv } from 'tailwind-variants';
5
+
6
+ type Props = {
7
+ color?: TextColor;
8
+ size?: Size;
9
+ variant?: 'filled';
10
+ children: Snippet;
11
+ };
12
+
13
+ const { size = 'medium', variant, color = 'secondary', children }: Props = $props();
14
+
15
+ const styles = tv({
16
+ base: 'font-monospace',
17
+ variants: {
18
+ textColor: {
19
+ muted: 'text-gray-600 dark:text-gray-400',
20
+ primary: 'text-primary',
21
+ secondary: 'text-dark',
22
+ success: 'text-success',
23
+ danger: 'text-danger',
24
+ warning: 'text-warning',
25
+ info: 'text-info',
26
+ },
27
+
28
+ filled: {
29
+ true: 'rounded px-2 py-1',
30
+ false: '',
31
+ },
32
+
33
+ filledColor: {
34
+ false: '',
35
+ muted: 'bg-gray-600 dark:bg-gray-800 text-light',
36
+ primary: 'bg-primary text-light',
37
+ secondary: 'bg-dark text-light',
38
+ success: 'bg-success text-light',
39
+ danger: 'bg-danger text-light',
40
+ warning: 'bg-warning text-light',
41
+ info: 'bg-info text-dark',
42
+ },
43
+
44
+ size: {
45
+ tiny: 'text-xs',
46
+ small: 'text-sm',
47
+ medium: 'text-md',
48
+ large: 'text-lg',
49
+ giant: 'text-xl',
50
+ },
51
+ },
52
+ });
53
+ </script>
54
+
55
+ <code
56
+ class={styles({
57
+ filled: variant === 'filled',
58
+ filledColor: variant === 'filled' && color,
59
+ textColor: color,
60
+ size,
61
+ })}>{@render children()}</code
62
+ >
@@ -0,0 +1,9 @@
1
+ import type { Size, TextColor } from '../../types.js';
2
+ import type { Snippet } from 'svelte';
3
+ declare const Code: import("svelte").Component<{
4
+ color?: TextColor;
5
+ size?: Size;
6
+ variant?: "filled";
7
+ children: Snippet;
8
+ }, {}, "">;
9
+ export default Code;
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
- import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
3
- import { mdiCheck, mdiMinus } from '@mdi/js';
2
+ import { getFieldContext } from '../../common/context.svelte.js';
4
3
  import Icon from '../Icon/Icon.svelte';
5
- import { tv } from 'tailwind-variants';
6
4
  import type { Color, Shape, Size } from '../../types.js';
7
- import { cleanClass } from '../../utils.js';
5
+ import { cleanClass, generateId } from '../../utils.js';
6
+ import { mdiCheck, mdiMinus } from '@mdi/js';
7
+ import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
8
+ import { tv } from 'tailwind-variants';
8
9
 
9
10
  type CheckboxProps = WithoutChildrenOrChild<CheckboxPrimitive.RootProps> & {
10
11
  color?: Color;
@@ -22,8 +23,16 @@
22
23
  ...restProps
23
24
  }: CheckboxProps = $props();
24
25
 
25
- const container = tv({
26
- base: 'ring-offset-background focus-visible:ring-ring peer box-content border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[disabled=true]:opacity-50 overflow-hidden',
26
+ const {
27
+ label,
28
+ readOnly = false,
29
+ required = false,
30
+ invalid = false,
31
+ disabled = false,
32
+ } = $derived(getFieldContext());
33
+
34
+ const containerStyles = tv({
35
+ base: 'border-2 ring-offset-background focus-visible:ring-ring peer box-content focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[state=checked]:bg-primary data-[disabled=true]:opacity-50 overflow-hidden',
27
36
  variants: {
28
37
  shape: {
29
38
  rectangle: 'rounded-none',
@@ -32,7 +41,7 @@
32
41
  },
33
42
  color: {
34
43
  primary: 'border-primary',
35
- secondary: 'border-secondary',
44
+ secondary: 'border-dark',
36
45
  success: 'border-success',
37
46
  danger: 'border-danger',
38
47
  warning: 'border-warning',
@@ -55,7 +64,7 @@
55
64
  },
56
65
  });
57
66
 
58
- const icon = tv({
67
+ const iconStyles = tv({
59
68
  variants: {
60
69
  fullWidth: {
61
70
  true: 'w-full',
@@ -70,24 +79,55 @@
70
79
  },
71
80
  },
72
81
  });
82
+
83
+ const labelStyles = tv({
84
+ base: '',
85
+ variants: {
86
+ size: {
87
+ tiny: 'text-xs',
88
+ small: 'text-sm',
89
+ medium: 'text-md',
90
+ large: 'text-lg',
91
+ giant: 'text-xl',
92
+ },
93
+ },
94
+ });
95
+
96
+ const id = generateId();
97
+ const inputId = `input-${id}`;
98
+ const labelId = `label-${id}`;
73
99
  </script>
74
100
 
75
- <CheckboxPrimitive.Root
76
- bind:ref
77
- class={cleanClass(
78
- container({ size, color, shape, roundedSize: shape === 'semi-round' ? size : undefined }),
79
- className,
80
- )}
81
- bind:checked
82
- {...restProps}
83
- >
84
- {#snippet children({ checked, indeterminate })}
85
- <div class={cleanClass('flex items-center justify-center text-current')}>
86
- {#if indeterminate}
87
- <Icon icon={mdiMinus} size="100%" class={cleanClass(icon({ color }))} />
88
- {:else if checked}
89
- <Icon icon={mdiCheck} size="100%" class={cleanClass(icon({ color }))} />
90
- {/if}
91
- </div>
92
- {/snippet}
93
- </CheckboxPrimitive.Root>
101
+ <div class="flex flex-col gap-1">
102
+ {#if label}
103
+ <label id={labelId} for={inputId} class={labelStyles({ size })}>{label}</label>
104
+ {/if}
105
+
106
+ <CheckboxPrimitive.Root
107
+ bind:ref
108
+ class={cleanClass(
109
+ containerStyles({
110
+ size,
111
+ color: invalid ? 'danger' : color,
112
+ shape,
113
+ roundedSize: shape === 'semi-round' ? size : undefined,
114
+ }),
115
+ className,
116
+ )}
117
+ bind:checked
118
+ disabled={disabled || readOnly}
119
+ {required}
120
+ aria-readonly={disabled || readOnly}
121
+ {...restProps}
122
+ >
123
+ {#snippet children({ checked, indeterminate })}
124
+ <div class={cleanClass('flex items-center justify-center text-current')}>
125
+ {#if indeterminate}
126
+ <Icon icon={mdiMinus} size="100%" class={cleanClass(iconStyles({ color }))} />
127
+ {:else if checked}
128
+ <Icon icon={mdiCheck} size="100%" class={cleanClass(iconStyles({ color }))} />
129
+ {/if}
130
+ </div>
131
+ {/snippet}
132
+ </CheckboxPrimitive.Root>
133
+ </div>
@@ -1,5 +1,5 @@
1
- import { Checkbox as CheckboxPrimitive } from 'bits-ui';
2
1
  import type { Color, Shape, Size } from '../../types.js';
2
+ import { Checkbox as CheckboxPrimitive } from 'bits-ui';
3
3
  declare const Checkbox: import("svelte").Component<Omit<Omit<CheckboxPrimitive.RootProps, "child">, "children"> & {
4
4
  color?: Color;
5
5
  shape?: Shape;
@@ -2,17 +2,17 @@
2
2
  import Text from '../Text/Text.svelte';
3
3
  import { ChildKey } from '../../constants.js';
4
4
  import Child from '../../internal/Child.svelte';
5
- import type { Color } from '../../types.js';
5
+ import type { TextColor } from '../../types.js';
6
6
  import { cleanClass } from '../../utils.js';
7
7
  import type { Snippet } from 'svelte';
8
8
 
9
9
  type Props = {
10
- color?: Color;
10
+ color?: TextColor;
11
11
  class?: string;
12
12
  children?: Snippet;
13
13
  };
14
14
 
15
- let { class: className, children, color }: Props = $props();
15
+ let { class: className, children, color = 'muted' }: Props = $props();
16
16
  </script>
17
17
 
18
18
  <Child for={ChildKey.Field} as={ChildKey.HelperText}>
@@ -1,7 +1,7 @@
1
- import type { Color } from '../../types.js';
1
+ import type { TextColor } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  declare const HelperText: import("svelte").Component<{
4
- color?: Color;
4
+ color?: TextColor;
5
5
  class?: string;
6
6
  children?: Snippet;
7
7
  }, {}, "">;
@@ -5,6 +5,7 @@
5
5
  import { tv } from 'tailwind-variants';
6
6
 
7
7
  let {
8
+ containerRef = $bindable(null),
8
9
  shape = 'semi-round',
9
10
  size = 'medium',
10
11
  class: className,
@@ -72,7 +73,7 @@
72
73
  const labelId = `label-${id}`;
73
74
  </script>
74
75
 
75
- <div class="flex flex-col gap-1">
76
+ <div class="flex flex-col gap-1" bind:this={containerRef}>
76
77
  {#if label}
77
78
  <label id={labelId} for={inputId} class={labelStyles({ size })}>{label}</label>
78
79
  {/if}
@@ -1,3 +1,3 @@
1
1
  import type { InputProps } from '../../types.js';
2
- declare const Input: import("svelte").Component<InputProps, {}, "value">;
2
+ declare const Input: import("svelte").Component<InputProps, {}, "value" | "containerRef">;
3
3
  export default Input;
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
- import { IconButton, Input, type PasswordInputProps } from '../../index.js';
2
+ import Input from './Input.svelte';
3
+ import IconButton from '../IconButton/IconButton.svelte';
4
+ import type { PasswordInputProps } from '../../types.js';
3
5
  import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
4
6
 
5
7
  let {
@@ -1,3 +1,3 @@
1
- import { type PasswordInputProps } from '../../index.js';
1
+ import type { PasswordInputProps } from '../../types.js';
2
2
  declare const PasswordInput: import("svelte").Component<PasswordInputProps, {}, "value" | "isVisible">;
3
3
  export default PasswordInput;
@@ -0,0 +1,16 @@
1
+ <script lang="ts">
2
+ import { getBytesWithUnit } from '../../utilities/byte-units.js';
3
+
4
+ type Props = {
5
+ bytes: number;
6
+ precision?: number;
7
+ variant?: 'short' | 'narrow';
8
+ };
9
+
10
+ const { bytes, precision, variant = 'short' }: Props = $props();
11
+
12
+ const [value, unit] = $derived(getBytesWithUnit(bytes, precision));
13
+ const separator = $derived(variant === 'narrow' ? '' : ' ');
14
+ </script>
15
+
16
+ <span>{value}{separator}{unit}</span>
@@ -0,0 +1,6 @@
1
+ declare const FormatBytes: import("svelte").Component<{
2
+ bytes: number;
3
+ precision?: number;
4
+ variant?: "short" | "narrow";
5
+ }, {}, "">;
6
+ export default FormatBytes;
@@ -1,12 +1,12 @@
1
1
  <script lang="ts">
2
- import type { Color, HeadingSize } from '../../types.js';
2
+ import type { HeadingColor, HeadingSize } from '../../types.js';
3
3
  import { cleanClass } from '../../utils.js';
4
4
  import type { Snippet } from 'svelte';
5
5
  import { tv } from 'tailwind-variants';
6
6
 
7
7
  type Props = {
8
8
  size: HeadingSize;
9
- color?: Color;
9
+ color?: HeadingColor;
10
10
  class?: string;
11
11
 
12
12
  children: Snippet;
@@ -27,6 +27,7 @@
27
27
  base: 'font-bold leading-none tracking-tight',
28
28
  variants: {
29
29
  color: {
30
+ muted: 'text-gray-600 dark:text-gray-400',
30
31
  primary: 'text-primary',
31
32
  secondary: 'text-dark',
32
33
  success: 'text-success',
@@ -1,8 +1,8 @@
1
- import type { Color, HeadingSize } from '../../types.js';
1
+ import type { HeadingColor, HeadingSize } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  declare const Heading: import("svelte").Component<{
4
4
  size: HeadingSize;
5
- color?: Color;
5
+ color?: HeadingColor;
6
6
  class?: string;
7
7
  children: Snippet;
8
8
  }, {}, "">;
@@ -1,30 +1,30 @@
1
1
  <script lang="ts">
2
- import icon from '../../assets/immich-logo.svg';
3
2
  import inlineDark from '../../assets/immich-logo-inline-dark.svg';
4
3
  import inlineLight from '../../assets/immich-logo-inline-light.svg';
5
4
  import stackedDark from '../../assets/immich-logo-stacked-dark.svg';
6
5
  import stackedLight from '../../assets/immich-logo-stacked-light.svg';
7
- import type { Size } from '../../types.js';
6
+ import icon from '../../assets/immich-logo.svg';
7
+ import { theme } from '../../services/theme.svelte.js';
8
+ import { Theme, type Size } from '../../types.js';
8
9
  import { cleanClass } from '../../utils.js';
9
10
  import { tv } from 'tailwind-variants';
10
11
 
11
12
  type Props = {
12
13
  size?: Size | 'landing';
13
- theme?: 'dark' | 'light';
14
14
  variant?: 'stacked' | 'inline' | 'logo' | 'icon';
15
15
  class?: string;
16
16
  };
17
17
 
18
- const { theme = 'light', variant = 'logo', size = 'medium', class: className }: Props = $props();
18
+ const { variant = 'logo', size = 'medium', class: className }: Props = $props();
19
19
 
20
- const getUrl = ({ theme, variant }: Required<Pick<Props, 'theme' | 'variant'>>): string => {
20
+ const getUrl = (variant: Props['variant']): string => {
21
21
  switch (variant) {
22
22
  case 'stacked': {
23
- return theme === 'light' ? stackedLight : stackedDark;
23
+ return theme.value === Theme.Light ? stackedLight : stackedDark;
24
24
  }
25
25
 
26
26
  case 'inline': {
27
- return theme === 'light' ? inlineLight : inlineDark;
27
+ return theme.value === Theme.Light ? inlineLight : inlineDark;
28
28
  }
29
29
 
30
30
  default: {
@@ -53,7 +53,7 @@
53
53
  },
54
54
  });
55
55
 
56
- const src = $derived(getUrl({ theme, variant }));
56
+ const src = $derived(getUrl(variant));
57
57
  </script>
58
58
 
59
59
  <img {src} class={cleanClass(styles({ size, variant }), className)} alt="Immich logo" />
@@ -1,7 +1,6 @@
1
- import type { Size } from '../../types.js';
1
+ import { type Size } from '../../types.js';
2
2
  declare const Logo: import("svelte").Component<{
3
3
  size?: Size | "landing";
4
- theme?: "dark" | "light";
5
4
  variant?: "stacked" | "inline" | "logo" | "icon";
6
5
  class?: string;
7
6
  }, {}, "">;
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import InternalSelect from '../../internal/Select.svelte';
3
+ import type { MultiSelectProps, SelectItem } from '../../types.js';
4
+
5
+ type T = SelectItem;
6
+
7
+ let { value = $bindable(), onChange, ...restProps }: MultiSelectProps<T> = $props();
8
+
9
+ const handleChange = (items: T[]) => {
10
+ value = items;
11
+ onChange?.(value);
12
+ };
13
+ </script>
14
+
15
+ <InternalSelect multiple onChange={handleChange} {...restProps} />
@@ -0,0 +1,3 @@
1
+ import type { MultiSelectProps, SelectItem } from '../../types.js';
2
+ declare const MultiSelect: import("svelte").Component<MultiSelectProps<SelectItem>, {}, "value">;
3
+ export default MultiSelect;
@@ -0,0 +1,12 @@
1
+ <script lang="ts">
2
+ type Props = {
3
+ title: string;
4
+ };
5
+
6
+ let { title }: Props = $props();
7
+ </script>
8
+
9
+ <div class="text-xs transition-all duration-200">
10
+ <p class="hidden px-6 py-4 uppercase group-hover:sm:block md:block">{title}</p>
11
+ <hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
12
+ </div>
@@ -0,0 +1,4 @@
1
+ declare const NavbarGroup: import("svelte").Component<{
2
+ title: string;
3
+ }, {}, "">;
4
+ export default NavbarGroup;
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import Icon from '../Icon/Icon.svelte';
3
+ import type { IconProps } from '../../types.js';
4
+ import { tv } from 'tailwind-variants';
5
+
6
+ type Props = {
7
+ title: string;
8
+ active?: boolean;
9
+ href: string;
10
+ } & IconProps;
11
+
12
+ let { href, title, active = false, ...iconProps }: Props = $props();
13
+
14
+ const styles = tv({
15
+ base: 'hover:bg-subtle hover:text-primary flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 pl-5 group-hover:sm:px-5 md:px-5',
16
+ variants: {
17
+ active: {
18
+ true: 'bg-primary/10 text-primary',
19
+ false: '',
20
+ },
21
+ },
22
+ });
23
+ </script>
24
+
25
+ <a {href} draggable="false" aria-current={active ? 'page' : undefined} class={styles({ active })}>
26
+ <div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
27
+ <Icon size="1.5em" class="shrink-0" aria-hidden={true} {...iconProps} />
28
+ <span class="text-sm font-medium">{title}</span>
29
+ </div>
30
+ </a>
@@ -0,0 +1,7 @@
1
+ import type { IconProps } from '../../types.js';
2
+ declare const NavbarItem: import("svelte").Component<{
3
+ title: string;
4
+ active?: boolean;
5
+ href: string;
6
+ } & IconProps, {}, "">;
7
+ export default NavbarItem;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { cleanClass } from '../../utils.js';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ type Props = {
6
+ class?: string;
7
+ children?: Snippet;
8
+ };
9
+
10
+ const { class: className, children }: Props = $props();
11
+ </script>
12
+
13
+ <div class={cleanClass('immich-scrollbar h-full w-full overflow-auto', className)}>
14
+ {@render children?.()}
15
+ </div>
16
+
17
+ <style>
18
+ /* width */
19
+ .immich-scrollbar::-webkit-scrollbar {
20
+ width: 8px;
21
+ height: 8px;
22
+ }
23
+
24
+ /* Track */
25
+ .immich-scrollbar::-webkit-scrollbar-track {
26
+ background: #f1f1f1;
27
+ border-radius: 16px;
28
+ }
29
+
30
+ /* Handle */
31
+ .immich-scrollbar::-webkit-scrollbar-thumb {
32
+ background: rgba(85, 86, 87, 0.408);
33
+ border-radius: 16px;
34
+ }
35
+
36
+ /* Handle on hover */
37
+ .immich-scrollbar::-webkit-scrollbar-thumb:hover {
38
+ background: #4250afad;
39
+ border-radius: 16px;
40
+ }
41
+ </style>
@@ -0,0 +1,6 @@
1
+ import type { Snippet } from 'svelte';
2
+ declare const Scrollable: import("svelte").Component<{
3
+ class?: string;
4
+ children?: Snippet;
5
+ }, {}, "">;
6
+ export default Scrollable;
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import InternalSelect from '../../internal/Select.svelte';
3
+ import type { SelectItem, SelectProps } from '../../types.js';
4
+
5
+ type T = SelectItem;
6
+
7
+ let { value = $bindable(), onChange, ...restProps }: SelectProps<T> = $props();
8
+
9
+ const handleChange = (items: T[]) => {
10
+ value = items[0] as T;
11
+ onChange?.(value);
12
+ };
13
+ </script>
14
+
15
+ <InternalSelect onChange={handleChange} {...restProps} />
@@ -0,0 +1,3 @@
1
+ import type { SelectItem, SelectProps } from '../../types.js';
2
+ declare const Select: import("svelte").Component<SelectProps<SelectItem>, {}, "value">;
3
+ export default Select;
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import { getFieldContext } from '../../common/context.svelte.js';
3
+ import type { Color } from '../../types.js';
4
+ import { cleanClass } from '../../utils.js';
5
+ import type { HTMLInputAttributes } from 'svelte/elements';
6
+ import { tv } from 'tailwind-variants';
7
+
8
+ type Props = {
9
+ checked?: boolean;
10
+ color?: Color;
11
+ disabled?: boolean;
12
+ class?: string;
13
+ onToggle?: ((checked: boolean) => void) | undefined;
14
+ } & HTMLInputAttributes;
15
+
16
+ let {
17
+ checked = $bindable(false),
18
+ class: className,
19
+ color = 'primary',
20
+ onToggle = undefined,
21
+ ...restProps
22
+ }: Props = $props();
23
+
24
+ const {
25
+ label,
26
+ readOnly = false,
27
+ required = false,
28
+ disabled = false,
29
+ } = $derived(getFieldContext());
30
+
31
+ const enabled = $derived(checked && !disabled);
32
+
33
+ const handleToggle = (event: Event) => onToggle?.((event.target as HTMLInputElement).checked);
34
+
35
+ const wrapper = tv({
36
+ base: 'relative flex flex-col justify-center',
37
+ variants: {
38
+ disabled: {
39
+ true: 'cursor-not-allowed',
40
+ false: 'cursor-pointer',
41
+ },
42
+ },
43
+ });
44
+
45
+ const bar = tv({
46
+ base: 'w-12 h-3 my-2 rounded-full border border-transparent',
47
+ variants: {
48
+ fillColor: {
49
+ default: 'bg-gray-400',
50
+ primary: 'bg-primary/50',
51
+ secondary: 'bg-dark/50',
52
+ success: 'bg-success/50',
53
+ danger: 'bg-danger/50',
54
+ warning: 'bg-warning/50',
55
+ info: 'bg-info/50',
56
+ },
57
+ },
58
+ });
59
+
60
+ const dot = tv({
61
+ base: 'absolute transition-colors h-6 w-6 rounded-full transition-transform duration-[400ms]',
62
+ variants: {
63
+ checked: {
64
+ true: 'translate-x-6',
65
+ false: '',
66
+ },
67
+ fillColor: {
68
+ default: 'bg-gray-600',
69
+ primary: 'bg-primary',
70
+ secondary: 'bg-dark',
71
+ success: 'bg-success',
72
+ danger: 'bg-danger',
73
+ warning: 'bg-warning',
74
+ info: 'bg-info',
75
+ },
76
+ },
77
+ });
78
+ </script>
79
+
80
+ <label class={cleanClass(className)}>
81
+ {label}
82
+ <span class={wrapper({ disabled })}>
83
+ <input
84
+ class="hidden"
85
+ type="checkbox"
86
+ bind:checked
87
+ onclick={handleToggle}
88
+ {required}
89
+ aria-required={required}
90
+ {disabled}
91
+ aria-disabled={disabled}
92
+ readonly={readOnly}
93
+ aria-readonly={readOnly}
94
+ {...restProps}
95
+ />
96
+ <span class={bar({ fillColor: enabled ? color : 'default' })}> </span>
97
+ <span class={dot({ checked: enabled, fillColor: enabled ? color : 'default' })}></span>
98
+ </span>
99
+ </label>