@immich/ui 0.53.3 → 0.55.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.
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import Button from '../Button/Button.svelte';
3
+ import Modal from '../Modal/Modal.svelte';
4
+ import ModalBody from '../Modal/ModalBody.svelte';
5
+ import ModalFooter from '../Modal/ModalFooter.svelte';
6
+ import { t } from '../../services/translation.svelte.js';
7
+ import type { Color, ModalSize } from '../../types.js';
8
+ import type { Snippet } from 'svelte';
9
+
10
+ type Props = {
11
+ title: string;
12
+ icon?: string | boolean;
13
+ closeText?: string;
14
+ closeColor?: Color;
15
+ size?: ModalSize;
16
+ onClose: () => void;
17
+ children: Snippet;
18
+ };
19
+
20
+ let {
21
+ title,
22
+ icon,
23
+ closeText = t('close'),
24
+ closeColor = 'secondary',
25
+ size = 'small',
26
+ onClose = () => {},
27
+ children,
28
+ }: Props = $props();
29
+ </script>
30
+
31
+ <Modal {title} {onClose} {size} {icon}>
32
+ <ModalBody {children} />
33
+ <ModalFooter>
34
+ <Button shape="round" color={closeColor} fullWidth onclick={onClose}>
35
+ {closeText}
36
+ </Button>
37
+ </ModalFooter>
38
+ </Modal>
@@ -0,0 +1,14 @@
1
+ import type { Color, ModalSize } from '../../types.js';
2
+ import type { Snippet } from 'svelte';
3
+ type Props = {
4
+ title: string;
5
+ icon?: string | boolean;
6
+ closeText?: string;
7
+ closeColor?: Color;
8
+ size?: ModalSize;
9
+ onClose: () => void;
10
+ children: Snippet;
11
+ };
12
+ declare const BasicModal: import("svelte").Component<Props, {}, "">;
13
+ type BasicModal = ReturnType<typeof BasicModal>;
14
+ export default BasicModal;
@@ -4,8 +4,8 @@
4
4
  import { zIndex } from '../../constants.js';
5
5
  import { styleVariants } from '../../styles.js';
6
6
  import { type ActionItem, type ContextMenuProps, type MenuItems } from '../../types.js';
7
- import { isMenuItemType } from '../../utilities/common.js';
8
- import { cleanClass, isEnabled } from '../../utilities/internal.js';
7
+ import { isEnabled, isMenuItemType } from '../../utilities/common.js';
8
+ import { cleanClass } from '../../utilities/internal.js';
9
9
  import { DropdownMenu } from 'bits-ui';
10
10
  import { fly } from 'svelte/transition';
11
11
  import { tv } from 'tailwind-variants';
@@ -18,8 +18,9 @@
18
18
  size?: ModalSize;
19
19
  preventDefault?: boolean;
20
20
  onClose: () => void;
21
+ onReset?: (event: Event) => void;
21
22
  onSubmit: (event: SubmitEvent) => void;
22
- children: Snippet;
23
+ children: Snippet<[{ formId: string }]>;
23
24
  };
24
25
 
25
26
  let {
@@ -31,6 +32,7 @@
31
32
  size = 'small',
32
33
  preventDefault = true,
33
34
  onClose = () => {},
35
+ onReset,
34
36
  onSubmit,
35
37
  children,
36
38
  }: Props = $props();
@@ -43,13 +45,21 @@
43
45
  onSubmit(event);
44
46
  };
45
47
 
48
+ const onreset = (event: Event) => {
49
+ if (preventDefault) {
50
+ event.preventDefault();
51
+ }
52
+
53
+ onReset?.(event);
54
+ };
55
+
46
56
  const formId = generateId();
47
57
  </script>
48
58
 
49
59
  <Modal {title} {onClose} {size} {icon}>
50
60
  <ModalBody>
51
- <form {onsubmit} id={formId}>
52
- {@render children()}
61
+ <form {onsubmit} {onreset} id={formId}>
62
+ {@render children({ formId })}
53
63
  </form>
54
64
  </ModalBody>
55
65
  <ModalFooter>
@@ -9,8 +9,11 @@ type Props = {
9
9
  size?: ModalSize;
10
10
  preventDefault?: boolean;
11
11
  onClose: () => void;
12
+ onReset?: (event: Event) => void;
12
13
  onSubmit: (event: SubmitEvent) => void;
13
- children: Snippet;
14
+ children: Snippet<[{
15
+ formId: string;
16
+ }]>;
14
17
  };
15
18
  declare const FormModal: import("svelte").Component<Props, {}, "">;
16
19
  type FormModal = ReturnType<typeof FormModal>;
@@ -0,0 +1,111 @@
1
+ <script lang="ts">
2
+ import IconButton from '../IconButton/IconButton.svelte';
3
+ import { zIndex } from '../../constants.js';
4
+ import { t } from '../../services/translation.svelte.js';
5
+ import type { CarouselImageItem, TranslationProps } from '../../types.js';
6
+ import { cleanClass } from '../../utilities/internal.js';
7
+ import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
8
+ import type { Snippet } from 'svelte';
9
+ import { fade } from 'svelte/transition';
10
+
11
+ type Props = {
12
+ items: CarouselImageItem[];
13
+ scrollBy?: number;
14
+ translations?: TranslationProps<'navigate_next' | 'navigate_previous'>;
15
+ class?: string;
16
+ child?: Snippet<[CarouselImageItem]>;
17
+ };
18
+
19
+ const { items, scrollBy = 400, translations, class: className, child = defaultChild }: Props = $props();
20
+
21
+ let shouldRender = $derived(items.length > 0);
22
+
23
+ let ref: HTMLElement | undefined = $state();
24
+ let offsetWidth = $state(0);
25
+ let innerWidth = $state(0);
26
+ let scrollPosition = $state(0);
27
+
28
+ let canScrollLeft = $derived(scrollPosition > 0);
29
+ let canScrollRight = $derived(Math.ceil(scrollPosition) < Math.floor(innerWidth - offsetWidth));
30
+
31
+ const onScroll = () => {
32
+ scrollPosition = ref?.scrollLeft ?? 0;
33
+ };
34
+
35
+ const scrollLeft = () => ref?.scrollBy({ left: -scrollBy, behavior: 'smooth' });
36
+ const scrollRight = () => ref?.scrollBy({ left: scrollBy, behavior: 'smooth' });
37
+ </script>
38
+
39
+ {#snippet defaultChild(item: CarouselImageItem)}
40
+ <a
41
+ class="item-card relative me-2 inline-block aspect-3/4 h-54 rounded-xl last:me-0 max-md:h-37.5 md:me-4 md:aspect-4/3 xl:aspect-video"
42
+ href={item.href}
43
+ >
44
+ <img class="h-full w-full rounded-xl object-cover" src={item.src} alt={item.alt ?? item.title} draggable="false" />
45
+ <div
46
+ class="absolute start-0 top-0 h-full w-full rounded-xl bg-linear-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
47
+ ></div>
48
+ <p class="absolute start-4 bottom-2 text-lg text-white max-md:text-sm">
49
+ {item.title}
50
+ </p>
51
+ </a>
52
+ {/snippet}
53
+
54
+ {#if shouldRender}
55
+ <section
56
+ bind:this={ref}
57
+ bind:clientWidth={offsetWidth}
58
+ class={cleanClass('relative mt-3 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all', className)}
59
+ style="scrollbar-width:none"
60
+ onscroll={onScroll}
61
+ >
62
+ {#if canScrollLeft || canScrollRight}
63
+ <div class="sticky start-0 {zIndex.CarouselImage}">
64
+ {#if canScrollLeft}
65
+ <div class="light absolute start-4 top-27 -translate-y-1/2 max-md:top-19" transition:fade={{ duration: 200 }}>
66
+ <IconButton
67
+ icon={mdiChevronLeft}
68
+ shape="round"
69
+ variant="outline"
70
+ color="secondary"
71
+ class="opacity-50 hover:opacity-100"
72
+ size="giant"
73
+ aria-label={t('navigate_previous', translations)}
74
+ onclick={scrollLeft}
75
+ />
76
+ </div>
77
+ {/if}
78
+ {#if canScrollRight}
79
+ <div
80
+ class="light absolute end-4 top-27 {zIndex.CarouselImage} -translate-y-1/2 max-md:top-19"
81
+ transition:fade={{ duration: 200 }}
82
+ >
83
+ <IconButton
84
+ icon={mdiChevronRight}
85
+ shape="round"
86
+ variant="outline"
87
+ color="secondary"
88
+ class="opacity-50 hover:opacity-100"
89
+ size="giant"
90
+ aria-label={t('navigate_next', translations)}
91
+ onclick={scrollRight}
92
+ />
93
+ </div>
94
+ {/if}
95
+ </div>
96
+ {/if}
97
+ <div class="inline-block" bind:clientWidth={innerWidth}>
98
+ {#each items as item, i (item.id ?? i)}
99
+ {@render child(item)}
100
+ {/each}
101
+ </div>
102
+ </section>
103
+ {/if}
104
+
105
+ <style>
106
+ .item-card {
107
+ box-shadow:
108
+ rgba(60, 64, 67, 0.3) 0px 1px 2px 0px,
109
+ rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
110
+ }
111
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { CarouselImageItem, TranslationProps } from '../../types.js';
2
+ import type { Snippet } from 'svelte';
3
+ type Props = {
4
+ items: CarouselImageItem[];
5
+ scrollBy?: number;
6
+ translations?: TranslationProps<'navigate_next' | 'navigate_previous'>;
7
+ class?: string;
8
+ child?: Snippet<[CarouselImageItem]>;
9
+ };
10
+ declare const ImageCarousel: import("svelte").Component<Props, {}, "">;
11
+ type ImageCarousel = ReturnType<typeof ImageCarousel>;
12
+ export default ImageCarousel;
@@ -59,6 +59,17 @@
59
59
  element.style.minHeight = '0';
60
60
  element.style.height = 'auto';
61
61
  element.style.height = `${element.scrollHeight}px`;
62
+
63
+ // Show scrollbar only if there is a max-height and content exceeds it
64
+ const maxHeight = Number.parseFloat(getComputedStyle(element).maxHeight);
65
+ const hasMaxHeight = maxHeight !== undefined;
66
+ if (hasMaxHeight && element.scrollHeight > maxHeight) {
67
+ element.style.overflow = 'auto';
68
+ } else if (hasMaxHeight && element.scrollHeight <= maxHeight) {
69
+ element.style.overflow = 'hidden';
70
+ } else {
71
+ element.style.overflow = '';
72
+ }
62
73
  }
63
74
  };
64
75
 
@@ -14,6 +14,7 @@ export declare enum ChildKey {
14
14
  ModalFooter = "modal-footer"
15
15
  }
16
16
  export declare const zIndex: {
17
+ CarouselImage: string;
17
18
  AppShellSidebar: string;
18
19
  ModalBackdrop: string;
19
20
  ModalContent: string;
package/dist/constants.js CHANGED
@@ -15,6 +15,7 @@ export var ChildKey;
15
15
  ChildKey["ModalFooter"] = "modal-footer";
16
16
  })(ChildKey || (ChildKey = {}));
17
17
  export const zIndex = {
18
+ CarouselImage: 'z-1',
18
19
  AppShellSidebar: 'z-30',
19
20
  ModalBackdrop: 'z-40',
20
21
  ModalContent: 'z-50',
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { default as AppShellHeader } from './components/AppShell/AppShellHeader.
17
17
  export { default as AppShellSidebar } from './components/AppShell/AppShellSidebar.svelte';
18
18
  export { default as Avatar } from './components/Avatar/Avatar.svelte';
19
19
  export { default as Badge } from './components/Badge/Badge.svelte';
20
+ export { default as BasicModal } from './components/BasicModal/BasicModal.svelte';
20
21
  export { default as Breadcrumbs } from './components/Breadcrumbs/Breadcrumbs.svelte';
21
22
  export { default as Button } from './components/Button/Button.svelte';
22
23
  export { default as Card } from './components/Card/Card.svelte';
@@ -42,6 +43,7 @@ export { default as Heading } from './components/Heading/Heading.svelte';
42
43
  export { default as HelperText } from './components/HelperText/HelperText.svelte';
43
44
  export { default as Icon } from './components/Icon/Icon.svelte';
44
45
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
46
+ export { default as ImageCarousel } from './components/ImageCarousel/ImageCarousel.svelte';
45
47
  export { default as Input } from './components/Input/Input.svelte';
46
48
  export { default as Kbd } from './components/Kbd/Kbd.svelte';
47
49
  export { default as Label } from './components/Label/Label.svelte';
@@ -76,6 +78,7 @@ export { default as ToastContent } from './components/Toast/ToastContent.svelte'
76
78
  export { default as ToastPanel } from './components/Toast/ToastPanel.svelte';
77
79
  export { default as Tooltip } from './components/Tooltip/Tooltip.svelte';
78
80
  export { default as TooltipProvider } from './components/Tooltip/TooltipProvider.svelte';
81
+ export * from './actions/shortcut.js';
79
82
  export * from './services/command-palette-manager.svelte.js';
80
83
  export * from './services/menu-manager.svelte.js';
81
84
  export * from './services/modal-manager.svelte.js';
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export { default as AppShellHeader } from './components/AppShell/AppShellHeader.
19
19
  export { default as AppShellSidebar } from './components/AppShell/AppShellSidebar.svelte';
20
20
  export { default as Avatar } from './components/Avatar/Avatar.svelte';
21
21
  export { default as Badge } from './components/Badge/Badge.svelte';
22
+ export { default as BasicModal } from './components/BasicModal/BasicModal.svelte';
22
23
  export { default as Breadcrumbs } from './components/Breadcrumbs/Breadcrumbs.svelte';
23
24
  export { default as Button } from './components/Button/Button.svelte';
24
25
  export { default as Card } from './components/Card/Card.svelte';
@@ -44,6 +45,7 @@ export { default as Heading } from './components/Heading/Heading.svelte';
44
45
  export { default as HelperText } from './components/HelperText/HelperText.svelte';
45
46
  export { default as Icon } from './components/Icon/Icon.svelte';
46
47
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
48
+ export { default as ImageCarousel } from './components/ImageCarousel/ImageCarousel.svelte';
47
49
  export { default as Input } from './components/Input/Input.svelte';
48
50
  export { default as Kbd } from './components/Kbd/Kbd.svelte';
49
51
  export { default as Label } from './components/Label/Label.svelte';
@@ -79,6 +81,7 @@ export { default as ToastPanel } from './components/Toast/ToastPanel.svelte';
79
81
  export { default as Tooltip } from './components/Tooltip/Tooltip.svelte';
80
82
  export { default as TooltipProvider } from './components/Tooltip/TooltipProvider.svelte';
81
83
  // helpers
84
+ export * from './actions/shortcut.js';
82
85
  export * from './services/command-palette-manager.svelte.js';
83
86
  export * from './services/menu-manager.svelte.js';
84
87
  export * from './services/modal-manager.svelte.js';
@@ -1,7 +1,8 @@
1
1
  import { matchesShortcut, shortcuts, shouldIgnoreEvent } from '../actions/shortcut.js';
2
2
  import CommandPaletteModal from '../internal/CommandPaletteModal.svelte';
3
3
  import { modalManager } from './modal-manager.svelte.js';
4
- import { asArray, generateId, getSearchString, isEnabled } from '../utilities/internal.js';
4
+ import { isEnabled } from '../utilities/common.js';
5
+ import { asArray, generateId, getSearchString } from '../utilities/internal.js';
5
6
  export const defaultProvider = ({ name, actions }) => ({
6
7
  name,
7
8
  onSearch: (query) => query ? actions.filter((action) => getSearchString(action).includes(query.toLowerCase())) : actions,
@@ -18,6 +18,8 @@ declare const defaultTranslations: {
18
18
  command_palette_to_navigate: string;
19
19
  command_palette_to_close: string;
20
20
  command_palette_to_show_all: string;
21
+ navigate_next: string;
22
+ navigate_previous: string;
21
23
  toast_success_title: string;
22
24
  toast_info_title: string;
23
25
  toast_warning_title: string;
@@ -25,6 +25,9 @@ const defaultTranslations = {
25
25
  command_palette_to_navigate: 'to navigate',
26
26
  command_palette_to_close: 'to close',
27
27
  command_palette_to_show_all: 'to show all',
28
+ // navigation
29
+ navigate_next: 'Next',
30
+ navigate_previous: 'Previous',
28
31
  toast_success_title: 'Success',
29
32
  toast_info_title: 'Info',
30
33
  toast_warning_title: 'Warning',
package/dist/types.d.ts CHANGED
@@ -275,4 +275,11 @@ export type BreadcrumbItem = {
275
275
  title?: string;
276
276
  icon: IconLike;
277
277
  });
278
+ export type CarouselImageItem = {
279
+ title: string;
280
+ href: string;
281
+ src: string;
282
+ alt?: string;
283
+ id?: string;
284
+ };
278
285
  export {};
@@ -1,4 +1,4 @@
1
- import { MenuItemType, type ActionItem } from '../types.js';
1
+ import { MenuItemType, type ActionItem, type IfLike } from '../types.js';
2
2
  import type { DateTime } from 'luxon';
3
3
  export declare const resolveUrl: (url: string, currentHostname?: string) => string;
4
4
  export declare const isExternalLink: (href: string) => boolean;
@@ -32,3 +32,4 @@ export declare const resolveMetadata: (site: Metadata, page?: Metadata, article?
32
32
  } | undefined;
33
33
  };
34
34
  export declare const asText: (...items: unknown[]) => string;
35
+ export declare const isEnabled: ({ $if }: IfLike) => boolean;
@@ -67,3 +67,9 @@ export const asText = (...items) => {
67
67
  .join('|')
68
68
  .toLowerCase();
69
69
  };
70
+ export const isEnabled = ({ $if }) => {
71
+ if (!$if) {
72
+ return true;
73
+ }
74
+ return !!$if();
75
+ };
@@ -1,4 +1,4 @@
1
- import type { ActionItem, Color, IconLike, IfLike, MaybeArray, TextColor } from '../types.js';
1
+ import type { ActionItem, Color, IconLike, MaybeArray, TextColor } from '../types.js';
2
2
  export declare const cleanClass: (...classNames: unknown[]) => string;
3
3
  export declare const withPrefix: (key: string) => string;
4
4
  export declare const generateId: () => string;
@@ -9,6 +9,5 @@ export declare const resolveIcon: ({ icons, color, override, fallback, }: {
9
9
  override?: IconLike | false;
10
10
  icons: Partial<Record<Color | TextColor, string>>;
11
11
  }) => IconLike | undefined;
12
- export declare const isEnabled: ({ $if }: IfLike) => boolean;
13
12
  export declare const asArray: <T>(items?: MaybeArray<T>) => T[];
14
13
  export declare const getSearchString: ({ title, description, type, searchText }: ActionItem) => string;
@@ -25,6 +25,5 @@ export const resolveIcon = ({ icons, color, override, fallback, }) => {
25
25
  }
26
26
  return icons[color] ?? fallback;
27
27
  };
28
- export const isEnabled = ({ $if }) => $if?.() ?? true;
29
28
  export const asArray = (items) => (Array.isArray(items) ? items : items ? [items] : []);
30
29
  export const getSearchString = ({ title, description, type, searchText }) => searchText ?? asText(title, description, type);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.53.3",
3
+ "version": "0.55.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "repository": {
6
6
  "type": "git",
@@ -58,7 +58,7 @@
58
58
  "@immich/svelte-markdown-preprocess": "^0.1.0"
59
59
  },
60
60
  "volta": {
61
- "node": "24.11.1"
61
+ "node": "24.12.0"
62
62
  },
63
63
  "scripts": {
64
64
  "create": "node scripts/create.js",