@immich/ui 0.26.0 → 0.27.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 (40) hide show
  1. package/dist/actions/shortcut.d.ts +28 -0
  2. package/dist/actions/shortcut.js +72 -0
  3. package/dist/components/CommandPalette/CommandPalette.svelte +150 -0
  4. package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +7 -0
  5. package/dist/components/CommandPalette/CommandPaletteItem.svelte +71 -0
  6. package/dist/components/CommandPalette/CommandPaletteItem.svelte.d.ts +10 -0
  7. package/dist/components/{Form → Input}/Input.svelte +3 -3
  8. package/dist/components/Link/Link.svelte +11 -2
  9. package/dist/components/Link/Link.svelte.d.ts +1 -0
  10. package/dist/components/Navbar/NavbarItem.svelte +8 -4
  11. package/dist/components/Navbar/NavbarItem.svelte.d.ts +2 -1
  12. package/dist/components/{Form → PasswordInput}/PasswordInput.svelte +1 -1
  13. package/dist/index.d.ts +14 -8
  14. package/dist/index.js +15 -8
  15. package/dist/internal/Button.svelte +3 -0
  16. package/dist/internal/Select.svelte +2 -2
  17. package/dist/services/command-palette-manager.svelte.d.ts +37 -0
  18. package/dist/services/command-palette-manager.svelte.js +92 -0
  19. package/dist/services/translation.svelte.d.ts +7 -3
  20. package/dist/services/translation.svelte.js +12 -3
  21. package/dist/site/SiteFooter.svelte +84 -0
  22. package/dist/site/SiteFooter.svelte.d.ts +18 -0
  23. package/dist/site/SiteFooterLink.svelte +18 -0
  24. package/dist/site/SiteFooterLink.svelte.d.ts +10 -0
  25. package/dist/site/constants.d.ts +42 -0
  26. package/dist/site/constants.js +120 -0
  27. package/dist/types.d.ts +8 -6
  28. package/dist/utils.d.ts +2 -0
  29. package/dist/utils.js +3 -0
  30. package/package.json +2 -1
  31. /package/dist/components/{Form → Checkbox}/Checkbox.svelte +0 -0
  32. /package/dist/components/{Form → Checkbox}/Checkbox.svelte.d.ts +0 -0
  33. /package/dist/components/{Form → Field}/Field.svelte +0 -0
  34. /package/dist/components/{Form → Field}/Field.svelte.d.ts +0 -0
  35. /package/dist/components/{Form → HelperText}/HelperText.svelte +0 -0
  36. /package/dist/components/{Form → HelperText}/HelperText.svelte.d.ts +0 -0
  37. /package/dist/components/{Form → Input}/Input.svelte.d.ts +0 -0
  38. /package/dist/components/{Form → PasswordInput}/PasswordInput.svelte.d.ts +0 -0
  39. /package/dist/services/{modalManager.svelte.d.ts → modal-manager.svelte.d.ts} +0 -0
  40. /package/dist/services/{modalManager.svelte.js → modal-manager.svelte.js} +0 -0
@@ -0,0 +1,28 @@
1
+ import type { ActionReturn } from 'svelte/action';
2
+ export type Shortcut = {
3
+ key: string;
4
+ alt?: boolean;
5
+ ctrl?: boolean;
6
+ shift?: boolean;
7
+ meta?: boolean;
8
+ };
9
+ export type ShortcutOptions<T = HTMLElement> = {
10
+ shortcut: Shortcut;
11
+ /** If true, the event handler will not execute if the event comes from an input field */
12
+ ignoreInputFields?: boolean;
13
+ onShortcut: (event: KeyboardEvent & {
14
+ currentTarget: T;
15
+ }) => unknown;
16
+ preventDefault?: boolean;
17
+ };
18
+ export declare const shortcutLabel: (shortcut: Shortcut) => string;
19
+ /** Determines whether an event should be ignored. The event will be ignored if:
20
+ * - The element dispatching the event is not the same as the element which the event listener is attached to
21
+ * - The element dispatching the event is an input field
22
+ */
23
+ export declare const shouldIgnoreEvent: (event: KeyboardEvent | ClipboardEvent) => boolean;
24
+ export declare const matchesShortcut: (event: KeyboardEvent, shortcut: Shortcut) => boolean;
25
+ /** Bind a single keyboard shortcut to node. */
26
+ export declare const shortcut: <T extends HTMLElement>(node: T, option: ShortcutOptions<T>) => ActionReturn<ShortcutOptions<T>>;
27
+ /** Binds multiple keyboard shortcuts to node */
28
+ export declare const shortcuts: <T extends HTMLElement>(node: T, options: ShortcutOptions<T>[]) => ActionReturn<ShortcutOptions<T>[]>;
@@ -0,0 +1,72 @@
1
+ export const shortcutLabel = (shortcut) => {
2
+ let label = '';
3
+ if (shortcut.ctrl) {
4
+ label += 'Ctrl ';
5
+ }
6
+ if (shortcut.alt) {
7
+ label += 'Alt ';
8
+ }
9
+ if (shortcut.meta) {
10
+ label += 'Cmd ';
11
+ }
12
+ if (shortcut.shift) {
13
+ label += '⇧';
14
+ }
15
+ label += shortcut.key.toUpperCase();
16
+ return label;
17
+ };
18
+ /** Determines whether an event should be ignored. The event will be ignored if:
19
+ * - The element dispatching the event is not the same as the element which the event listener is attached to
20
+ * - The element dispatching the event is an input field
21
+ */
22
+ export const shouldIgnoreEvent = (event) => {
23
+ if (event.target === event.currentTarget) {
24
+ return false;
25
+ }
26
+ const type = event.target.type;
27
+ return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type);
28
+ };
29
+ export const matchesShortcut = (event, shortcut) => {
30
+ return (shortcut.key.toLowerCase() === event.key.toLowerCase() &&
31
+ Boolean(shortcut.alt) === event.altKey &&
32
+ Boolean(shortcut.ctrl) === event.ctrlKey &&
33
+ Boolean(shortcut.shift) === event.shiftKey &&
34
+ Boolean(shortcut.meta) === event.metaKey);
35
+ };
36
+ /** Bind a single keyboard shortcut to node. */
37
+ export const shortcut = (node, option) => {
38
+ const { update: shortcutsUpdate, destroy } = shortcuts(node, [option]);
39
+ return {
40
+ update(newOption) {
41
+ shortcutsUpdate?.([newOption]);
42
+ },
43
+ destroy,
44
+ };
45
+ };
46
+ /** Binds multiple keyboard shortcuts to node */
47
+ export const shortcuts = (node, options) => {
48
+ function onKeydown(event) {
49
+ const ignoreShortcut = shouldIgnoreEvent(event);
50
+ for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true, } of options) {
51
+ if (ignoreInputFields && ignoreShortcut) {
52
+ continue;
53
+ }
54
+ if (matchesShortcut(event, shortcut)) {
55
+ if (preventDefault) {
56
+ event.preventDefault();
57
+ }
58
+ onShortcut(event);
59
+ return;
60
+ }
61
+ }
62
+ }
63
+ node.addEventListener('keydown', onKeydown);
64
+ return {
65
+ update(newOptions) {
66
+ options = newOptions;
67
+ },
68
+ destroy() {
69
+ node.removeEventListener('keydown', onKeydown);
70
+ },
71
+ };
72
+ };
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import { shortcuts } from '../../actions/shortcut.js';
3
+ import CloseButton from '../CloseButton/CloseButton.svelte';
4
+ import CommandPaletteItem from './CommandPaletteItem.svelte';
5
+ import Icon from '../Icon/Icon.svelte';
6
+ import Input from '../Input/Input.svelte';
7
+ import Modal from '../Modal/Modal.svelte';
8
+ import ModalBody from '../Modal/ModalBody.svelte';
9
+ import ModalFooter from '../Modal/ModalFooter.svelte';
10
+ import ModalHeader from '../Modal/ModalHeader.svelte';
11
+ import Stack from '../Stack/Stack.svelte';
12
+ import Text from '../Text/Text.svelte';
13
+ import { commandPaletteManager } from '../../services/command-palette-manager.svelte';
14
+ import { t } from '../../services/translation.svelte.js';
15
+ import type { TranslationProps } from '../../types.js';
16
+ import { mdiArrowDown, mdiArrowUp, mdiKeyboardEsc, mdiKeyboardReturn, mdiMagnify } from '@mdi/js';
17
+
18
+ type Props = {
19
+ translations?: TranslationProps<
20
+ | 'search_placeholder'
21
+ | 'search_no_results'
22
+ | 'search_recently_used'
23
+ | 'command_palette_prompt_default'
24
+ >;
25
+ };
26
+
27
+ let { translations }: Props = $props();
28
+
29
+ let inputElement = $state<HTMLInputElement | null>(null);
30
+
31
+ const handleOpen = () => commandPaletteManager.open();
32
+ const handleClose = () => commandPaletteManager.close();
33
+ const handleUp = (event: KeyboardEvent) => handleNavigate(event, 'up');
34
+ const handleDown = (event: KeyboardEvent) => handleNavigate(event, 'down');
35
+ const handleSelect = (event: KeyboardEvent) => handleNavigate(event, 'select');
36
+ const handleNavigate = async (event: KeyboardEvent, direction: 'up' | 'down' | 'select') => {
37
+ if (!commandPaletteManager.isOpen) {
38
+ return;
39
+ }
40
+
41
+ event.preventDefault();
42
+
43
+ switch (direction) {
44
+ case 'up': {
45
+ commandPaletteManager.up();
46
+ break;
47
+ }
48
+
49
+ case 'down': {
50
+ commandPaletteManager.down();
51
+ break;
52
+ }
53
+
54
+ case 'select': {
55
+ await commandPaletteManager.select();
56
+ break;
57
+ }
58
+ }
59
+ };
60
+ </script>
61
+
62
+ <svelte:window
63
+ use:shortcuts={[
64
+ { shortcut: { key: 'k', meta: true }, preventDefault: true, onShortcut: handleOpen },
65
+ { shortcut: { key: 'k', ctrl: true }, preventDefault: true, onShortcut: handleOpen },
66
+ { shortcut: { key: '/' }, preventDefault: true, onShortcut: handleOpen },
67
+ { shortcut: { key: 'ArrowUp' }, ignoreInputFields: false, onShortcut: handleUp },
68
+ { shortcut: { key: 'ArrowDown' }, ignoreInputFields: false, onShortcut: handleDown },
69
+ { shortcut: { key: 'k', ctrl: true }, ignoreInputFields: false, onShortcut: handleUp },
70
+ { shortcut: { key: 'k', meta: true }, ignoreInputFields: false, onShortcut: handleUp },
71
+ { shortcut: { key: 'j', ctrl: true }, ignoreInputFields: false, onShortcut: handleDown },
72
+ { shortcut: { key: 'j', meta: true }, ignoreInputFields: false, onShortcut: handleDown },
73
+ { shortcut: { key: 'Enter' }, ignoreInputFields: false, onShortcut: handleSelect },
74
+ { shortcut: { key: 'Escape' }, onShortcut: handleClose },
75
+ ]}
76
+ />
77
+
78
+ {#if commandPaletteManager.isOpen}
79
+ <Modal size="large" onClose={handleClose} closeOnBackdropClick>
80
+ <ModalHeader>
81
+ <div class="flex place-items-center gap-1">
82
+ <Input
83
+ bind:ref={inputElement}
84
+ bind:value={commandPaletteManager.query}
85
+ placeholder={t('search_placeholder', translations)}
86
+ leadingIcon={mdiMagnify}
87
+ tabindex={1}
88
+ />
89
+ <div>
90
+ <CloseButton onclick={() => commandPaletteManager.close()} class="md:hidden" />
91
+ </div>
92
+ </div>
93
+ </ModalHeader>
94
+ <ModalBody>
95
+ <Stack gap={2}>
96
+ {#if commandPaletteManager.query}
97
+ {#if commandPaletteManager.results.length === 0}
98
+ <Text>{t('search_no_results', translations)}</Text>
99
+ {/if}
100
+ {:else if commandPaletteManager.recentItems.length > 0}
101
+ <Text>{t('search_recently_used', translations)}</Text>
102
+ {:else}
103
+ <Text>{t('command_palette_prompt_default', translations)}</Text>
104
+ {/if}
105
+
106
+ {#if commandPaletteManager.results.length > 0}
107
+ <div class="flex flex-col">
108
+ {#each commandPaletteManager.results as item, i (i)}
109
+ <CommandPaletteItem
110
+ {item}
111
+ selected={commandPaletteManager.selectedIndex === i}
112
+ onRemove={commandPaletteManager.query
113
+ ? undefined
114
+ : () => commandPaletteManager.remove(i)}
115
+ onSelect={() => commandPaletteManager.select(i)}
116
+ />
117
+ {/each}
118
+ </div>
119
+ {/if}
120
+ </Stack>
121
+ </ModalBody>
122
+ <ModalFooter>
123
+ <div class="flex w-full justify-around">
124
+ <div class="flex gap-4">
125
+ <div class="flex place-items-center gap-1">
126
+ <span class="rounded bg-gray-300 p-1 dark:bg-gray-500">
127
+ <Icon icon={mdiKeyboardReturn} size="1rem" />
128
+ </span>
129
+ <Text size="small">to select</Text>
130
+ </div>
131
+
132
+ <div class="flex place-items-center gap-1">
133
+ <span class="flex gap-1 rounded bg-gray-300 p-1 dark:bg-gray-500">
134
+ <Icon icon={mdiArrowUp} size="1rem" />
135
+ <Icon icon={mdiArrowDown} size="1rem" />
136
+ </span>
137
+ <Text size="small">to navigate</Text>
138
+ </div>
139
+
140
+ <div class="flex place-items-center gap-1">
141
+ <span class="rounded bg-gray-300 p-1 dark:bg-gray-500">
142
+ <Icon icon={mdiKeyboardEsc} size="1rem" />
143
+ </span>
144
+ <Text size="small">to close</Text>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </ModalFooter>
149
+ </Modal>
150
+ {/if}
@@ -0,0 +1,7 @@
1
+ import type { TranslationProps } from '../../types.js';
2
+ type Props = {
3
+ translations?: TranslationProps<'search_placeholder' | 'search_no_results' | 'search_recently_used' | 'command_palette_prompt_default'>;
4
+ };
5
+ declare const CommandPalette: import("svelte").Component<Props, {}, "">;
6
+ type CommandPalette = ReturnType<typeof CommandPalette>;
7
+ export default CommandPalette;
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import Button from '../Button/Button.svelte';
3
+ import Icon from '../Icon/Icon.svelte';
4
+ import IconButton from '../IconButton/IconButton.svelte';
5
+ import Text from '../Text/Text.svelte';
6
+ import type { CommandItem } from '../../services/command-palette-manager.svelte';
7
+ import { mdiClose } from '@mdi/js';
8
+
9
+ type Props = {
10
+ item: CommandItem;
11
+ selected: boolean;
12
+ onSelect: () => void;
13
+ onRemove?: () => void;
14
+ };
15
+
16
+ const { item, selected, onRemove, onSelect }: Props = $props();
17
+
18
+ const handleRemove = (event: MouseEvent) => {
19
+ event.stopPropagation();
20
+ onRemove?.();
21
+ };
22
+
23
+ let ref = $state<HTMLElement | null>(null);
24
+
25
+ $effect(() => {
26
+ if (selected && ref) {
27
+ ref.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'smooth' });
28
+ }
29
+ });
30
+ </script>
31
+
32
+ <div bind:this={ref} class="p-1">
33
+ <Button
34
+ onclick={() => onSelect()}
35
+ fullWidth
36
+ variant={selected ? 'outline' : 'ghost'}
37
+ color="secondary"
38
+ class="overflow-hidden border"
39
+ >
40
+ <div class="flex w-full place-items-center justify-between gap-2">
41
+ <div class="flex min-w-0 place-items-center gap-2">
42
+ <Icon icon={item.icon} size="2rem" class={item.iconClass} />
43
+ <div class="flex min-w-0 flex-col">
44
+ <div class="flex place-items-center gap-1">
45
+ <Text fontWeight="bold">{item.title}</Text>
46
+ </div>
47
+ {#if item.description}
48
+ <Text
49
+ size="small"
50
+ class="overflow-hidden text-ellipsis whitespace-nowrap"
51
+ color={selected ? undefined : 'muted'}>{item.description}</Text
52
+ >
53
+ {/if}
54
+ </div>
55
+ </div>
56
+ {#if onRemove}
57
+ <IconButton
58
+ size="small"
59
+ onclick={handleRemove}
60
+ icon={mdiClose}
61
+ shape="round"
62
+ variant="ghost"
63
+ color="secondary"
64
+ aria-label="Remove"
65
+ />
66
+ {:else}
67
+ <span class="shrink-0">[{item.type}]</span>
68
+ {/if}
69
+ </div>
70
+ </Button>
71
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { CommandItem } from '../../services/command-palette-manager.svelte';
2
+ type Props = {
3
+ item: CommandItem;
4
+ selected: boolean;
5
+ onSelect: () => void;
6
+ onRemove?: () => void;
7
+ };
8
+ declare const CommandPaletteItem: import("svelte").Component<Props, {}, "">;
9
+ type CommandPaletteItem = ReturnType<typeof CommandPaletteItem>;
10
+ export default CommandPaletteItem;
@@ -3,7 +3,7 @@
3
3
  import Label from '../Label/Label.svelte';
4
4
  import Text from '../Text/Text.svelte';
5
5
  import type { InputProps } from '../../types.js';
6
- import { cleanClass, generateId } from '../../utils.js';
6
+ import { cleanClass, generateId, isIconLike } from '../../utils.js';
7
7
  import Icon from '../Icon/Icon.svelte';
8
8
  import { tv } from 'tailwind-variants';
9
9
 
@@ -102,7 +102,7 @@
102
102
  {#if leadingIcon}
103
103
  <div tabindex="-1" class={iconStyles({ size })}>
104
104
  {#if leadingIcon}
105
- {#if typeof leadingIcon === 'string'}
105
+ {#if isIconLike(leadingIcon)}
106
106
  <Icon size="60%" icon={leadingIcon} />
107
107
  {:else}
108
108
  {@render leadingIcon()}
@@ -139,7 +139,7 @@
139
139
  {#if trailingIcon}
140
140
  <div tabindex="-1" class={cleanClass(iconStyles({ size }), 'end-0')}>
141
141
  {#if trailingIcon}
142
- {#if typeof trailingIcon === 'string'}
142
+ {#if isIconLike(trailingIcon)}
143
143
  <Icon size="60%" icon={trailingIcon} />
144
144
  {:else}
145
145
  {@render trailingIcon()}
@@ -7,15 +7,24 @@
7
7
  class?: string;
8
8
  children: Snippet;
9
9
  href: string;
10
+ underline?: boolean;
10
11
  external?: boolean;
11
12
  } & HTMLAnchorAttributes;
12
13
 
13
- const { href, class: className, external, children, ...restProps }: Props = $props();
14
+ const {
15
+ href,
16
+ class: className,
17
+ underline = true,
18
+ external,
19
+ children,
20
+ ...restProps
21
+ }: Props = $props();
14
22
  </script>
15
23
 
16
24
  <a
17
25
  {href}
18
- class={cleanClass('underline', className)}
26
+ draggable="false"
27
+ class={cleanClass(underline && 'underline', className)}
19
28
  target={external ? '_blank' : undefined}
20
29
  rel={external ? 'noopener noreferrer' : undefined}
21
30
  {...restProps}
@@ -4,6 +4,7 @@ type Props = {
4
4
  class?: string;
5
5
  children: Snippet;
6
6
  href: string;
7
+ underline?: boolean;
7
8
  external?: boolean;
8
9
  } & HTMLAnchorAttributes;
9
10
  declare const Link: import("svelte").Component<Props, {}, "">;
@@ -1,13 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { page } from '$app/state';
3
3
  import Icon from '../Icon/Icon.svelte';
4
+ import Link from '../Link/Link.svelte';
4
5
  import type { IconProps } from '../../types.js';
5
6
  import { tv } from 'tailwind-variants';
6
7
 
7
8
  type Props = {
8
9
  title: string;
9
- active?: boolean;
10
10
  href: string;
11
+ external?: boolean;
12
+ active?: boolean;
11
13
  variant?: 'compact';
12
14
  isActive?: () => boolean;
13
15
  icon?: string | IconProps;
@@ -18,6 +20,7 @@
18
20
 
19
21
  let {
20
22
  href,
23
+ external,
21
24
  isActive: isActiveOverride,
22
25
  title,
23
26
  variant,
@@ -49,10 +52,11 @@
49
52
  });
50
53
  </script>
51
54
 
52
- <a
55
+ <Link
53
56
  {href}
54
- draggable="false"
57
+ {external}
55
58
  aria-current={active ? 'page' : undefined}
59
+ underline={false}
56
60
  class={styles({ active, variant: variant ?? 'default' })}
57
61
  >
58
62
  <div class="flex w-full place-items-center gap-4 truncate overflow-hidden">
@@ -66,4 +70,4 @@
66
70
  {/if}
67
71
  <span class="text-sm font-medium">{title}</span>
68
72
  </div>
69
- </a>
73
+ </Link>
@@ -1,8 +1,9 @@
1
1
  import type { IconProps } from '../../types.js';
2
2
  type Props = {
3
3
  title: string;
4
- active?: boolean;
5
4
  href: string;
5
+ external?: boolean;
6
+ active?: boolean;
6
7
  variant?: 'compact';
7
8
  isActive?: () => boolean;
8
9
  icon?: string | IconProps;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
- import Input from './Input.svelte';
3
2
  import IconButton from '../IconButton/IconButton.svelte';
3
+ import Input from '../Input/Input.svelte';
4
4
  import { t } from '../../services/translation.svelte.js';
5
5
  import type { PasswordInputProps } from '../../types.js';
6
6
  import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
package/dist/index.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  export { default as appStoreBadge } from './assets/appstore-badge.svg';
2
2
  export { default as fdroidBadge } from './assets/fdroid-badge.svg';
3
+ export { default as immichFutoDark } from './assets/immich-logo-futo-dark.svg';
4
+ export { default as immichFutoLight } from './assets/immich-logo-futo-light.svg';
3
5
  export { default as immichLogoInlineDark } from './assets/immich-logo-inline-dark.svg';
4
6
  export { default as immichLogoInlineLight } from './assets/immich-logo-inline-light.svg';
5
7
  export { default as immichLogoStackedDark } from './assets/immich-logo-stacked-dark.svg';
6
8
  export { default as immichLogoStackedLight } from './assets/immich-logo-stacked-light.svg';
7
9
  export { default as immichLogoJson } from './assets/immich-logo.json';
8
10
  export { default as immichLogo } from './assets/immich-logo.svg';
9
- export { default as playStoreBadge } from './assets/playstore-badge.png';
10
11
  export { default as obtainiumBadge } from './assets/obtainium-badge.png';
12
+ export { default as playStoreBadge } from './assets/playstore-badge.png';
11
13
  export { default as Alert } from './components/Alert/Alert.svelte';
12
14
  export { default as AppShell } from './components/AppShell/AppShell.svelte';
13
15
  export { default as AppShellHeader } from './components/AppShell/AppShellHeader.svelte';
@@ -20,31 +22,32 @@ export { default as CardDescription } from './components/Card/CardDescription.sv
20
22
  export { default as CardFooter } from './components/Card/CardFooter.svelte';
21
23
  export { default as CardHeader } from './components/Card/CardHeader.svelte';
22
24
  export { default as CardTitle } from './components/Card/CardTitle.svelte';
25
+ export { default as Checkbox } from './components/Checkbox/Checkbox.svelte';
23
26
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
24
27
  export { default as Code } from './components/Code/Code.svelte';
28
+ export { default as CommandPalette } from './components/CommandPalette/CommandPalette.svelte';
25
29
  export { default as ConfirmModal } from './components/ConfirmModal/ConfirmModal.svelte';
26
30
  export { default as Container } from './components/Container/Container.svelte';
27
- export { default as Checkbox } from './components/Form/Checkbox.svelte';
28
- export { default as Field } from './components/Form/Field.svelte';
29
- export { default as HelperText } from './components/Form/HelperText.svelte';
30
- export { default as Input } from './components/Form/Input.svelte';
31
- export { default as PasswordInput } from './components/Form/PasswordInput.svelte';
31
+ export { default as Field } from './components/Field/Field.svelte';
32
32
  export { default as FormatBytes } from './components/FormatBytes/FormatBytes.svelte';
33
33
  export { default as Heading } from './components/Heading/Heading.svelte';
34
+ export { default as HelperText } from './components/HelperText/HelperText.svelte';
34
35
  export { default as Icon } from './components/Icon/Icon.svelte';
35
36
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
37
+ export { default as Input } from './components/Input/Input.svelte';
36
38
  export { default as Kbd } from './components/Kbd/Kbd.svelte';
37
39
  export { default as Label } from './components/Label/Label.svelte';
38
40
  export { default as Link } from './components/Link/Link.svelte';
39
41
  export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
40
42
  export { default as Logo } from './components/Logo/Logo.svelte';
41
43
  export { default as Modal } from './components/Modal/Modal.svelte';
42
- export { default as ModalHeader } from './components/Modal/ModalHeader.svelte';
43
44
  export { default as ModalBody } from './components/Modal/ModalBody.svelte';
44
45
  export { default as ModalFooter } from './components/Modal/ModalFooter.svelte';
46
+ export { default as ModalHeader } from './components/Modal/ModalHeader.svelte';
45
47
  export { default as MultiSelect } from './components/MultiSelect/MultiSelect.svelte';
46
48
  export { default as NavbarGroup } from './components/Navbar/NavbarGroup.svelte';
47
49
  export { default as NavbarItem } from './components/Navbar/NavbarItem.svelte';
50
+ export { default as PasswordInput } from './components/PasswordInput/PasswordInput.svelte';
48
51
  export { default as Scrollable } from './components/Scrollable/Scrollable.svelte';
49
52
  export { default as Select } from './components/Select/Select.svelte';
50
53
  export { default as HStack } from './components/Stack/HStack.svelte';
@@ -54,8 +57,11 @@ export { default as SupporterBadge } from './components/SupporterBadge/Supporter
54
57
  export { default as Switch } from './components/Switch/Switch.svelte';
55
58
  export { default as Text } from './components/Text/Text.svelte';
56
59
  export { default as ThemeSwitcher } from './components/ThemeSwitcher/ThemeSwitcher.svelte';
60
+ export * from './services/command-palette-manager.svelte.js';
61
+ export * from './services/modal-manager.svelte.js';
57
62
  export * from './services/theme.svelte.js';
58
63
  export * from './services/translation.svelte.js';
59
64
  export * from './types.js';
60
65
  export * from './utilities/byte-units.js';
61
- export * from './services/modalManager.svelte.js';
66
+ export * from './site/constants.js';
67
+ export { default as SiteFooter } from './site/SiteFooter.svelte';
package/dist/index.js CHANGED
@@ -1,14 +1,16 @@
1
1
  // files
2
2
  export { default as appStoreBadge } from './assets/appstore-badge.svg';
3
3
  export { default as fdroidBadge } from './assets/fdroid-badge.svg';
4
+ export { default as immichFutoDark } from './assets/immich-logo-futo-dark.svg';
5
+ export { default as immichFutoLight } from './assets/immich-logo-futo-light.svg';
4
6
  export { default as immichLogoInlineDark } from './assets/immich-logo-inline-dark.svg';
5
7
  export { default as immichLogoInlineLight } from './assets/immich-logo-inline-light.svg';
6
8
  export { default as immichLogoStackedDark } from './assets/immich-logo-stacked-dark.svg';
7
9
  export { default as immichLogoStackedLight } from './assets/immich-logo-stacked-light.svg';
8
10
  export { default as immichLogoJson } from './assets/immich-logo.json';
9
11
  export { default as immichLogo } from './assets/immich-logo.svg';
10
- export { default as playStoreBadge } from './assets/playstore-badge.png';
11
12
  export { default as obtainiumBadge } from './assets/obtainium-badge.png';
13
+ export { default as playStoreBadge } from './assets/playstore-badge.png';
12
14
  // components
13
15
  export { default as Alert } from './components/Alert/Alert.svelte';
14
16
  export { default as AppShell } from './components/AppShell/AppShell.svelte';
@@ -22,31 +24,32 @@ export { default as CardDescription } from './components/Card/CardDescription.sv
22
24
  export { default as CardFooter } from './components/Card/CardFooter.svelte';
23
25
  export { default as CardHeader } from './components/Card/CardHeader.svelte';
24
26
  export { default as CardTitle } from './components/Card/CardTitle.svelte';
27
+ export { default as Checkbox } from './components/Checkbox/Checkbox.svelte';
25
28
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
26
29
  export { default as Code } from './components/Code/Code.svelte';
30
+ export { default as CommandPalette } from './components/CommandPalette/CommandPalette.svelte';
27
31
  export { default as ConfirmModal } from './components/ConfirmModal/ConfirmModal.svelte';
28
32
  export { default as Container } from './components/Container/Container.svelte';
29
- export { default as Checkbox } from './components/Form/Checkbox.svelte';
30
- export { default as Field } from './components/Form/Field.svelte';
31
- export { default as HelperText } from './components/Form/HelperText.svelte';
32
- export { default as Input } from './components/Form/Input.svelte';
33
- export { default as PasswordInput } from './components/Form/PasswordInput.svelte';
33
+ export { default as Field } from './components/Field/Field.svelte';
34
34
  export { default as FormatBytes } from './components/FormatBytes/FormatBytes.svelte';
35
35
  export { default as Heading } from './components/Heading/Heading.svelte';
36
+ export { default as HelperText } from './components/HelperText/HelperText.svelte';
36
37
  export { default as Icon } from './components/Icon/Icon.svelte';
37
38
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
39
+ export { default as Input } from './components/Input/Input.svelte';
38
40
  export { default as Kbd } from './components/Kbd/Kbd.svelte';
39
41
  export { default as Label } from './components/Label/Label.svelte';
40
42
  export { default as Link } from './components/Link/Link.svelte';
41
43
  export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
42
44
  export { default as Logo } from './components/Logo/Logo.svelte';
43
45
  export { default as Modal } from './components/Modal/Modal.svelte';
44
- export { default as ModalHeader } from './components/Modal/ModalHeader.svelte';
45
46
  export { default as ModalBody } from './components/Modal/ModalBody.svelte';
46
47
  export { default as ModalFooter } from './components/Modal/ModalFooter.svelte';
48
+ export { default as ModalHeader } from './components/Modal/ModalHeader.svelte';
47
49
  export { default as MultiSelect } from './components/MultiSelect/MultiSelect.svelte';
48
50
  export { default as NavbarGroup } from './components/Navbar/NavbarGroup.svelte';
49
51
  export { default as NavbarItem } from './components/Navbar/NavbarItem.svelte';
52
+ export { default as PasswordInput } from './components/PasswordInput/PasswordInput.svelte';
50
53
  export { default as Scrollable } from './components/Scrollable/Scrollable.svelte';
51
54
  export { default as Select } from './components/Select/Select.svelte';
52
55
  export { default as HStack } from './components/Stack/HStack.svelte';
@@ -57,8 +60,12 @@ export { default as Switch } from './components/Switch/Switch.svelte';
57
60
  export { default as Text } from './components/Text/Text.svelte';
58
61
  export { default as ThemeSwitcher } from './components/ThemeSwitcher/ThemeSwitcher.svelte';
59
62
  // helpers
63
+ export * from './services/command-palette-manager.svelte.js';
64
+ export * from './services/modal-manager.svelte.js';
60
65
  export * from './services/theme.svelte.js';
61
66
  export * from './services/translation.svelte.js';
62
67
  export * from './types.js';
63
68
  export * from './utilities/byte-units.js';
64
- export * from './services/modalManager.svelte.js';
69
+ // site
70
+ export * from './site/constants.js';
71
+ export { default as SiteFooter } from './site/SiteFooter.svelte';
@@ -17,6 +17,7 @@
17
17
  ref = $bindable(null),
18
18
  type = 'button',
19
19
  href,
20
+ external,
20
21
  variant = 'filled',
21
22
  color = 'primary',
22
23
  shape = 'semi-round',
@@ -158,6 +159,8 @@
158
159
  {href}
159
160
  class={classList}
160
161
  aria-disabled={disabled}
162
+ target={external ? '_blank' : undefined}
163
+ rel={external ? 'noopener noreferrer' : undefined}
161
164
  {...restProps as HTMLAnchorAttributes}
162
165
  >
163
166
  {#if loading}
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { getFieldContext } from '../common/context.svelte.js';
3
- import Field from '../components/Form/Field.svelte';
4
- import Input from '../components/Form/Input.svelte';
3
+ import Field from '../components/Field/Field.svelte';
5
4
  import Icon from '../components/Icon/Icon.svelte';
6
5
  import IconButton from '../components/IconButton/IconButton.svelte';
6
+ import Input from '../components/Input/Input.svelte';
7
7
  import Label from '../components/Label/Label.svelte';
8
8
  import type { SelectCommonProps, SelectItem } from '../types.js';
9
9
  import { cleanClass, generateId } from '../utils.js';
@@ -0,0 +1,37 @@
1
+ export type CommandItem = {
2
+ icon: string;
3
+ iconClass: string;
4
+ type: string;
5
+ title: string;
6
+ description?: string;
7
+ text: string;
8
+ } & ({
9
+ href: string;
10
+ external?: boolean;
11
+ } | {
12
+ action: () => void;
13
+ });
14
+ export declare const asText: (...items: unknown[]) => string;
15
+ declare class CommandPaletteManager {
16
+ isEnabled: boolean;
17
+ isOpen: boolean;
18
+ query: string;
19
+ selectedIndex: number;
20
+ private normalizedQuery;
21
+ items: CommandItem[];
22
+ filteredItems: CommandItem[];
23
+ recentItems: CommandItem[];
24
+ results: CommandItem[];
25
+ enable(): void;
26
+ open(): Promise<void>;
27
+ close(): void;
28
+ select(selectedIndex?: number): Promise<void>;
29
+ remove(index: number): void;
30
+ up(): void;
31
+ down(): void;
32
+ reset(): void;
33
+ addCommands(itemOrItems: CommandItem | CommandItem[]): void;
34
+ removeCommands(itemOrItems: CommandItem | CommandItem[]): void;
35
+ }
36
+ export declare const commandPaletteManager: CommandPaletteManager;
37
+ export {};
@@ -0,0 +1,92 @@
1
+ import { goto } from '$app/navigation';
2
+ export const asText = (...items) => {
3
+ return items
4
+ .filter((item) => item !== undefined && item !== null)
5
+ .map((items) => String(items))
6
+ .join('|')
7
+ .toLowerCase();
8
+ };
9
+ const isEqual = (a, b) => {
10
+ return a.title === b.title && a.type === b.type;
11
+ };
12
+ const isMatch = (item, query) => {
13
+ if (!query) {
14
+ return true;
15
+ }
16
+ return item.text.includes(query);
17
+ };
18
+ class CommandPaletteManager {
19
+ isEnabled = $state(false);
20
+ isOpen = $state(false);
21
+ query = $state('');
22
+ selectedIndex = $state(0);
23
+ normalizedQuery = $derived(this.query.toLowerCase());
24
+ items = [];
25
+ filteredItems = $derived(this.items.filter((item) => isMatch(item, this.normalizedQuery)).slice(0, 100));
26
+ recentItems = $state([]);
27
+ results = $derived(this.query ? this.filteredItems : this.recentItems);
28
+ enable() {
29
+ this.isEnabled = true;
30
+ }
31
+ async open() {
32
+ if (!this.isEnabled || this.isOpen) {
33
+ return;
34
+ }
35
+ this.selectedIndex = 0;
36
+ this.isOpen = true;
37
+ }
38
+ close() {
39
+ if (!this.isEnabled || !this.isOpen) {
40
+ return;
41
+ }
42
+ this.query = '';
43
+ this.isOpen = false;
44
+ }
45
+ async select(selectedIndex) {
46
+ const selected = this.results[selectedIndex ?? this.selectedIndex];
47
+ if (!selected) {
48
+ return;
49
+ }
50
+ // no duplicates
51
+ this.recentItems = this.recentItems.filter((item) => !isEqual(item, selected));
52
+ this.recentItems.unshift(selected);
53
+ this.recentItems = this.recentItems.slice(0, 5);
54
+ if ('href' in selected) {
55
+ if (selected.href.startsWith('http') || selected.external) {
56
+ window.open(selected.href, '_blank');
57
+ }
58
+ else {
59
+ await goto(selected.href);
60
+ }
61
+ }
62
+ else {
63
+ await selected.action();
64
+ }
65
+ this.close();
66
+ }
67
+ remove(index) {
68
+ this.recentItems.splice(index, 1);
69
+ }
70
+ up() {
71
+ this.selectedIndex = (this.selectedIndex - 1 + this.results.length) % this.results.length;
72
+ }
73
+ down() {
74
+ this.selectedIndex = (this.selectedIndex + 1) % this.results.length;
75
+ }
76
+ reset() {
77
+ this.items = [];
78
+ this.isOpen = false;
79
+ this.query = '';
80
+ }
81
+ addCommands(itemOrItems) {
82
+ const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
83
+ this.items.push(...items);
84
+ }
85
+ removeCommands(itemOrItems) {
86
+ const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
87
+ for (const remoteItem of items) {
88
+ this.items = this.items.filter((item) => !isEqual(item, remoteItem));
89
+ }
90
+ }
91
+ }
92
+ export const commandPaletteManager = new CommandPaletteManager();
@@ -1,12 +1,16 @@
1
1
  import type { TranslationProps } from '../types.js';
2
2
  declare const defaultTranslations: {
3
+ cancel: string;
3
4
  close: string;
5
+ confirm: string;
6
+ search_placeholder: string;
7
+ search_no_results: string;
8
+ search_recently_used: string;
9
+ prompt_default: string;
4
10
  show_password: string;
5
11
  hide_password: string;
6
12
  dark_theme: string;
7
- confirm: string;
8
- prompt_default: string;
9
- cancel: string;
13
+ command_palette_prompt_default: string;
10
14
  };
11
15
  export type Translations = typeof defaultTranslations;
12
16
  export declare const translate: <T extends keyof Translations>(key: T, overrides?: TranslationProps<T>) => string;
@@ -1,11 +1,20 @@
1
1
  const defaultTranslations = {
2
+ // common
3
+ cancel: 'Cancel',
2
4
  close: 'Close',
5
+ confirm: 'Confirm',
6
+ search_placeholder: 'Search...',
7
+ search_no_results: 'No results',
8
+ search_recently_used: 'Recently used',
9
+ // modal
10
+ prompt_default: 'Are you sure you want to do this?',
11
+ // password input
3
12
  show_password: 'Show password',
4
13
  hide_password: 'Hide password',
14
+ // theme switcher
5
15
  dark_theme: 'Toggle dark theme',
6
- confirm: 'Confirm',
7
- prompt_default: 'Are you sure you want to do this?',
8
- cancel: 'Cancel',
16
+ // command palette
17
+ command_palette_prompt_default: 'Quickly find pages, actions, or commands',
9
18
  };
10
19
  let translations = $state(defaultTranslations);
11
20
  export const translate = (key, overrides) => overrides?.[key] ?? translations[key];
@@ -0,0 +1,84 @@
1
+ <script lang="ts">
2
+ import Heading from '../components/Heading/Heading.svelte';
3
+ import Stack from '../components/Stack/Stack.svelte';
4
+ import VStack from '../components/Stack/VStack.svelte';
5
+ import Text from '../components/Text/Text.svelte';
6
+ import ThemeSwitcher from '../components/ThemeSwitcher/ThemeSwitcher.svelte';
7
+ import { Constants } from './constants.js';
8
+ import SiteFooterLink from './SiteFooterLink.svelte';
9
+ import {
10
+ mdiBookshelf,
11
+ mdiChartGantt,
12
+ mdiCubeOutline,
13
+ mdiHomeSearchOutline,
14
+ mdiKey,
15
+ mdiOfficeBuildingOutline,
16
+ mdiScriptTextOutline,
17
+ mdiServerOutline,
18
+ mdiShoppingOutline,
19
+ } from '@mdi/js';
20
+ import { siAndroid, siApple, siDiscord, siGithub, siReddit } from 'simple-icons';
21
+ </script>
22
+
23
+ <div class="bg-dark/10 mt-16 rounded-t-3xl p-8">
24
+ <div class="mx-auto max-w-(--breakpoint-lg) xl:py-8">
25
+ <Stack gap={8}>
26
+ <div class="place-center grid grid-cols-2 gap-8 md:grid-cols-3 lg:grid-cols-5">
27
+ <Stack>
28
+ <Heading size="tiny">Social</Heading>
29
+ <SiteFooterLink href={Constants.Socials.Github} icon={siGithub} text="GitHub" />
30
+ <SiteFooterLink href={Constants.Socials.Discord} icon={siDiscord} text="Discord" />
31
+ <SiteFooterLink href={Constants.Socials.Reddit} icon={siReddit} text="Reddit" />
32
+ </Stack>
33
+
34
+ <Stack>
35
+ <Heading size="tiny">Download</Heading>
36
+ <SiteFooterLink href={Constants.Get.Android} icon={siAndroid} text="Android" />
37
+ <SiteFooterLink href={Constants.Get.iOS} icon={siApple} text="iOS" />
38
+ <SiteFooterLink
39
+ href={Constants.Get.DockerCompose}
40
+ icon={mdiServerOutline}
41
+ text="Server"
42
+ />
43
+ </Stack>
44
+
45
+ <Stack>
46
+ <Heading size="tiny">Company</Heading>
47
+ <SiteFooterLink
48
+ href={Constants.Socials.Futo}
49
+ icon={mdiOfficeBuildingOutline}
50
+ text="FUTO"
51
+ />
52
+ <SiteFooterLink href={Constants.Sites.Buy} icon={mdiKey} text="Purchase" />
53
+ <SiteFooterLink href={Constants.Sites.Store} icon={mdiShoppingOutline} text="Merch" />
54
+ </Stack>
55
+
56
+ <Stack>
57
+ <Heading size="tiny">Sites</Heading>
58
+ <SiteFooterLink
59
+ href={Constants.Sites.Docs}
60
+ icon={mdiScriptTextOutline}
61
+ text="Documentation"
62
+ />
63
+ <SiteFooterLink href={Constants.Sites.My} icon={mdiHomeSearchOutline} text="My Immich" />
64
+ <SiteFooterLink href={Constants.Sites.Api} icon={mdiCubeOutline} text="Immich API" />
65
+ </Stack>
66
+
67
+ <Stack>
68
+ <Heading size="tiny">Miscellaneous</Heading>
69
+ <SiteFooterLink href={Constants.Pages.Roadmap} icon={mdiChartGantt} text="Roadmap" />
70
+ <SiteFooterLink
71
+ href={Constants.Pages.CursedKnowledge}
72
+ icon={mdiBookshelf}
73
+ text="Cursed Knowledge"
74
+ />
75
+ </Stack>
76
+ </div>
77
+ <VStack class="text-center">
78
+ <Text size="large">This project is available under GNU AGPL v3 license.</Text>
79
+ <Text color="muted" variant="italic">Privacy should not be a luxury</Text>
80
+ <ThemeSwitcher color="secondary" />
81
+ </VStack>
82
+ </Stack>
83
+ </div>
84
+ </div>
@@ -0,0 +1,18 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const SiteFooter: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type SiteFooter = InstanceType<typeof SiteFooter>;
18
+ export default SiteFooter;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { HStack, Icon, Link, Text } from '../index.ts';
3
+
4
+ type Props = {
5
+ href: string;
6
+ icon: string | { path: string };
7
+ text: string;
8
+ };
9
+
10
+ let { href, icon, text }: Props = $props();
11
+ </script>
12
+
13
+ <Link {href} external>
14
+ <HStack>
15
+ <Icon {icon} size="1.5em" />
16
+ <Text>{text}</Text>
17
+ </HStack>
18
+ </Link>
@@ -0,0 +1,10 @@
1
+ type Props = {
2
+ href: string;
3
+ icon: string | {
4
+ path: string;
5
+ };
6
+ text: string;
7
+ };
8
+ declare const SiteFooterLink: import("svelte").Component<Props, {}, "">;
9
+ type SiteFooterLink = ReturnType<typeof SiteFooterLink>;
10
+ export default SiteFooterLink;
@@ -0,0 +1,42 @@
1
+ export declare const Constants: {
2
+ Socials: {
3
+ Futo: string;
4
+ Github: string;
5
+ Discord: string;
6
+ Reddit: string;
7
+ Weblate: string;
8
+ };
9
+ Get: {
10
+ iOS: string;
11
+ Android: string;
12
+ FDroid: string;
13
+ GithubRelease: string;
14
+ DockerCompose: string;
15
+ };
16
+ Sites: {
17
+ Api: string;
18
+ Demo: string;
19
+ My: string;
20
+ Buy: string;
21
+ Get: string;
22
+ Docs: string;
23
+ Store: string;
24
+ };
25
+ Pages: {
26
+ CursedKnowledge: string;
27
+ Roadmap: string;
28
+ };
29
+ Npm: {
30
+ Sdk: string;
31
+ };
32
+ };
33
+ export declare const siteCommands: {
34
+ icon: string;
35
+ type: string;
36
+ iconClass: string;
37
+ title: string;
38
+ description: string;
39
+ href: string;
40
+ external: boolean;
41
+ text: string;
42
+ }[];
@@ -0,0 +1,120 @@
1
+ import { asText } from '../services/command-palette-manager.svelte.js';
2
+ import { mdiOpenInNew } from '@mdi/js';
3
+ export const Constants = {
4
+ Socials: {
5
+ Futo: 'https://futo.org/',
6
+ Github: 'https://github.com/immich-app/immich',
7
+ Discord: 'https://discord.immich.app/',
8
+ Reddit: 'https://www.reddit.com/r/immich/',
9
+ Weblate: 'https://hosted.weblate.org/projects/immich/immich/',
10
+ },
11
+ Get: {
12
+ iOS: 'https://get.immich.app/ios',
13
+ Android: 'https://get.immich.app/android',
14
+ FDroid: 'https://get.immich.app/fdroid',
15
+ GithubRelease: 'https://github.com/immich-app/immich/releases/latest',
16
+ DockerCompose: 'https://get.immich.app/docker-compose',
17
+ },
18
+ Sites: {
19
+ Api: 'https://api.immich.app/',
20
+ Demo: 'https://demo.immich.app/',
21
+ My: 'https://my.immich.app/',
22
+ Buy: 'https://buy.immich.app/',
23
+ Get: 'https://get.immich.app/',
24
+ Docs: 'https://docs.immich.app/',
25
+ Store: 'https://immich.store/',
26
+ },
27
+ Pages: {
28
+ CursedKnowledge: 'https://immich.app/cursed-knowledge',
29
+ Roadmap: 'https://immich.app/roadmap',
30
+ },
31
+ Npm: {
32
+ Sdk: 'https://www.npmjs.com/package/@immich/sdk',
33
+ },
34
+ };
35
+ export const siteCommands = [
36
+ {
37
+ title: 'Immich Documentation',
38
+ description: 'View the Immich documentation',
39
+ href: Constants.Sites.Docs,
40
+ },
41
+ {
42
+ title: 'Buy Immich',
43
+ description: 'Support Immich by buying a product key.',
44
+ href: Constants.Sites.Buy,
45
+ },
46
+ {
47
+ title: 'My Immich',
48
+ description: 'Immich link proxy to redirect to your personal instance',
49
+ href: Constants.Sites.My,
50
+ },
51
+ {
52
+ title: 'Get Immich',
53
+ description: 'View downloads links for Immich apps and server',
54
+ href: Constants.Sites.Get,
55
+ },
56
+ {
57
+ title: 'Immich on the PlayStore',
58
+ description: 'View Immich on the Google Play Store',
59
+ href: Constants.Get.Android,
60
+ },
61
+ {
62
+ title: 'Immich on the iOS App Store',
63
+ description: 'View Immich on the iOS App Store',
64
+ href: Constants.Get.iOS,
65
+ },
66
+ {
67
+ title: 'Immich Demo',
68
+ description: 'Test out Immich with our public demo server',
69
+ href: Constants.Sites.Demo,
70
+ },
71
+ {
72
+ title: 'Immich Store',
73
+ description: 'Support the project by purchasing Immich merchandise',
74
+ href: Constants.Sites.Store,
75
+ },
76
+ {
77
+ title: 'Cursed Knowledge',
78
+ description: 'View our collection of cursed knowledge',
79
+ href: Constants.Pages.CursedKnowledge,
80
+ },
81
+ {
82
+ title: 'Roadmap',
83
+ description: 'View our project roadmap',
84
+ href: Constants.Pages.Roadmap,
85
+ },
86
+ {
87
+ title: 'reddit',
88
+ description: 'Join the Immich community on reddit',
89
+ href: Constants.Socials.Reddit,
90
+ },
91
+ {
92
+ title: 'GitHub',
93
+ description: 'View our project on GitHub',
94
+ href: Constants.Socials.Github,
95
+ },
96
+ {
97
+ title: 'Discord',
98
+ description: 'Join the conversation on Discord',
99
+ href: Constants.Socials.Discord,
100
+ },
101
+ {
102
+ title: 'Weblate',
103
+ description: 'Support the project by translating Immich on Weblate',
104
+ href: Constants.Socials.Weblate,
105
+ },
106
+ {
107
+ title: 'FUTO',
108
+ description: 'Learn more about FUTO, the company behind Immich',
109
+ href: Constants.Sites.Docs,
110
+ },
111
+ ].map((site) => ({
112
+ icon: mdiOpenInNew,
113
+ type: 'Link',
114
+ iconClass: 'text-indigo-700 dark:text-indigo-200',
115
+ title: site.title,
116
+ description: site.description,
117
+ href: site.href,
118
+ external: true,
119
+ text: asText('Site', 'Link', site.title, site.description, site.href),
120
+ }));
package/dist/types.d.ts CHANGED
@@ -21,11 +21,11 @@ export declare enum Theme {
21
21
  export type TranslationProps<T extends keyof Translations> = {
22
22
  [K in T]?: string;
23
23
  };
24
- type PathLike = {
24
+ export type IconLike = string | {
25
25
  path: string;
26
26
  };
27
27
  export type IconProps = {
28
- icon: string | PathLike;
28
+ icon: IconLike;
29
29
  title?: string;
30
30
  description?: string;
31
31
  size?: string;
@@ -40,8 +40,10 @@ export type IconProps = {
40
40
  };
41
41
  type ButtonOrAnchor = ({
42
42
  href?: never;
43
+ external?: never;
43
44
  } & HTMLButtonAttributes) | ({
44
45
  href: string;
46
+ external?: boolean;
45
47
  } & HTMLAnchorAttributes);
46
48
  type ButtonBase = {
47
49
  size?: Size;
@@ -54,8 +56,8 @@ export type ButtonProps = ButtonBase & {
54
56
  ref?: HTMLElement | null;
55
57
  fullWidth?: boolean;
56
58
  loading?: boolean;
57
- leadingIcon?: string;
58
- trailingIcon?: string;
59
+ leadingIcon?: IconLike;
60
+ trailingIcon?: IconLike;
59
61
  } & ButtonOrAnchor;
60
62
  export type CloseButtonProps = {
61
63
  size?: Size;
@@ -113,8 +115,8 @@ type BaseInputProps = {
113
115
  export type InputProps = BaseInputProps & {
114
116
  containerRef?: HTMLElement | null;
115
117
  type?: HTMLInputAttributes['type'];
116
- leadingIcon?: string | Snippet;
117
- trailingIcon?: string | Snippet;
118
+ leadingIcon?: IconLike | Snippet;
119
+ trailingIcon?: IconLike | Snippet;
118
120
  };
119
121
  export type PasswordInputProps = BaseInputProps & {
120
122
  translations?: TranslationProps<'show_password' | 'hide_password'>;
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { IconLike } from './types.js';
1
2
  export declare const cleanClass: (...classNames: unknown[]) => string;
2
3
  export declare const withPrefix: (key: string) => string;
3
4
  export declare const generateId: () => string;
5
+ export declare const isIconLike: (icon: unknown) => icon is IconLike;
package/dist/utils.js CHANGED
@@ -12,3 +12,6 @@ export const cleanClass = (...classNames) => {
12
12
  export const withPrefix = (key) => `immich-ui-${key}`;
13
13
  let _count = 0;
14
14
  export const generateId = () => `ui-id-${_count++}`;
15
+ export const isIconLike = (icon) => {
16
+ return typeof icon === 'string' || !!(icon && typeof icon === 'object' && 'path' in icon);
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "scripts": {
6
6
  "create": "node scripts/create.js",
@@ -69,6 +69,7 @@
69
69
  "dependencies": {
70
70
  "@mdi/js": "^7.4.47",
71
71
  "bits-ui": "^2.0.0",
72
+ "simple-icons": "^15.14.0",
72
73
  "tailwind-merge": "^3.0.0",
73
74
  "tailwind-variants": "^3.0.0"
74
75
  },
File without changes