@immich/ui 0.64.1 → 0.65.1

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.
@@ -12,6 +12,8 @@
12
12
  icon?: string | boolean;
13
13
  closeText?: string;
14
14
  closeColor?: Color;
15
+ closeOnBackdropClick?: boolean;
16
+ closeOnEsc?: boolean;
15
17
  size?: ModalSize;
16
18
  onClose: () => void;
17
19
  children: Snippet;
@@ -22,13 +24,15 @@
22
24
  icon,
23
25
  closeText = t('close'),
24
26
  closeColor = 'secondary',
27
+ closeOnBackdropClick = true,
28
+ closeOnEsc = true,
25
29
  size = 'small',
26
30
  onClose = () => {},
27
31
  children,
28
32
  }: Props = $props();
29
33
  </script>
30
34
 
31
- <Modal {title} {onClose} {size} {icon}>
35
+ <Modal {title} {onClose} {size} {icon} {closeOnBackdropClick} {closeOnEsc}>
32
36
  <ModalBody {children} />
33
37
  <ModalFooter>
34
38
  <Button shape="round" color={closeColor} fullWidth onclick={onClose}>
@@ -5,6 +5,8 @@ type Props = {
5
5
  icon?: string | boolean;
6
6
  closeText?: string;
7
7
  closeColor?: Color;
8
+ closeOnBackdropClick?: boolean;
9
+ closeOnEsc?: boolean;
8
10
  size?: ModalSize;
9
11
  onClose: () => void;
10
12
  children: Snippet;
@@ -4,7 +4,7 @@
4
4
  import type { ActionItem } from '../../types.js';
5
5
 
6
6
  type Props = {
7
- name: string;
7
+ name?: string;
8
8
  actions?: ActionItem[];
9
9
  };
10
10
 
@@ -1,6 +1,6 @@
1
1
  import type { ActionItem } from '../../types.js';
2
2
  type Props = {
3
- name: string;
3
+ name?: string;
4
4
  actions?: ActionItem[];
5
5
  };
6
6
  declare const CommandPaletteDefaultProvider: import("svelte").Component<Props, {}, "">;
@@ -6,7 +6,6 @@
6
6
  import Kbd from '../Kbd/Kbd.svelte';
7
7
  import Text from '../Text/Text.svelte';
8
8
  import type { ActionItem } from '../../types.js';
9
- import { cleanClass } from '../../utilities/internal.js';
10
9
 
11
10
  type Props = {
12
11
  item: ActionItem;
@@ -63,7 +62,7 @@
63
62
  <div class="flex shrink-0 flex-col justify-end gap-1">
64
63
  {#each renderedShortcuts as shortcut (shortcut.join('-'))}
65
64
  <div class="flex justify-end">
66
- <Kbd size="tiny" class={cleanClass(selected && 'border')}>{shortcut.join(' ')}</Kbd>
65
+ <Kbd size="tiny">{shortcut.join(' ')}</Kbd>
67
66
  </div>
68
67
  {/each}
69
68
  </div>
@@ -25,6 +25,7 @@
25
25
  expandable?: boolean;
26
26
  closeOnEsc?: boolean;
27
27
  closeOnBackdropClick?: boolean;
28
+ focusOnOpen?: boolean;
28
29
  children: Snippet;
29
30
  onClose?: () => void;
30
31
  onEscapeKeydown?: (event: KeyboardEvent) => void;
@@ -40,6 +41,7 @@
40
41
  class: className,
41
42
  closeOnEsc = true,
42
43
  closeOnBackdropClick = false,
44
+ focusOnOpen = false,
43
45
  children,
44
46
  onOpenAutoFocus,
45
47
  }: Props = $props();
@@ -96,6 +98,14 @@
96
98
  onEscapeKeydown?.(event);
97
99
  };
98
100
 
101
+ const handleOpenAutoFocus = (event: Event) => {
102
+ if (onOpenAutoFocus) {
103
+ onOpenAutoFocus(event);
104
+ } else if (!focusOnOpen) {
105
+ event.preventDefault();
106
+ }
107
+ };
108
+
99
109
  onMount(() => {
100
110
  layer = modalState.incrementLayer();
101
111
 
@@ -107,7 +117,7 @@
107
117
  <Dialog.Portal>
108
118
  <Dialog.Overlay class="{zIndex.ModalBackdrop} fixed start-0 top-0 flex h-dvh max-h-dvh w-screen bg-black/30" />
109
119
  <Dialog.Content
110
- {onOpenAutoFocus}
120
+ onOpenAutoFocus={handleOpenAutoFocus}
111
121
  onEscapeKeydown={handleEscapeKeydown}
112
122
  {escapeKeydownBehavior}
113
123
  {interactOutsideBehavior}
@@ -8,6 +8,7 @@ type Props = {
8
8
  expandable?: boolean;
9
9
  closeOnEsc?: boolean;
10
10
  closeOnBackdropClick?: boolean;
11
+ focusOnOpen?: boolean;
11
12
  children: Snippet;
12
13
  onClose?: () => void;
13
14
  onEscapeKeydown?: (event: KeyboardEvent) => void;
@@ -2,6 +2,7 @@
2
2
  import { shortcuts } from '../actions/shortcut.js';
3
3
  import CloseButton from '../components/CloseButton/CloseButton.svelte';
4
4
  import CommandPaletteItem from '../components/CommandPalette/CommandPaletteItem.svelte';
5
+ import Heading from '../components/Heading/Heading.svelte';
5
6
  import Icon from '../components/Icon/Icon.svelte';
6
7
  import Input from '../components/Input/Input.svelte';
7
8
  import Modal from '../components/Modal/Modal.svelte';
@@ -26,6 +27,10 @@
26
27
 
27
28
  const { onClose, translations, initialQuery = '' }: Props = $props();
28
29
 
30
+ let query = $state(initialQuery);
31
+
32
+ $effect(() => commandPaletteManager.queryUpdate(query));
33
+
29
34
  const handleUp = (event: KeyboardEvent) => handleNavigate(event, 'up');
30
35
  const handleDown = (event: KeyboardEvent) => handleNavigate(event, 'down');
31
36
  const handleSelect = (event: KeyboardEvent) => handleNavigate(event, 'select');
@@ -34,31 +39,26 @@
34
39
 
35
40
  switch (direction) {
36
41
  case 'up': {
37
- selectedIndex = Math.max((selectedIndex === 0 ? commandPaletteManager.items.length : selectedIndex) - 1, 0);
42
+ commandPaletteManager.navigateUp();
38
43
  break;
39
44
  }
40
45
 
41
46
  case 'down': {
42
- if (!query && commandPaletteManager.items.length === 0) {
47
+ if (!query && commandPaletteManager.results.length === 0) {
43
48
  commandPaletteManager.loadAllItems();
44
49
  break;
45
50
  }
46
51
 
47
- selectedIndex = (selectedIndex + 1) % commandPaletteManager.items.length || 0;
52
+ commandPaletteManager.navigateDown();
48
53
  break;
49
54
  }
50
55
 
51
56
  case 'select': {
52
- onClose(commandPaletteManager.items[selectedIndex]);
57
+ onClose(commandPaletteManager.selectedItem);
53
58
  break;
54
59
  }
55
60
  }
56
61
  };
57
-
58
- let selectedIndex = $state(0);
59
- let query = $state(initialQuery);
60
-
61
- $effect(() => commandPaletteManager.queryUpdate(query));
62
62
  </script>
63
63
 
64
64
  <svelte:window
@@ -73,7 +73,7 @@
73
73
  ]}
74
74
  />
75
75
 
76
- <Modal size="large" {onClose} closeOnBackdropClick class="max-h-[85vh] lg:max-h-[75vh]">
76
+ <Modal size="large" {onClose} closeOnBackdropClick focusOnOpen class="max-h-[85vh] lg:max-h-[75vh]">
77
77
  <ModalHeader>
78
78
  <div class="flex place-items-center gap-1">
79
79
  <Input
@@ -90,25 +90,34 @@
90
90
  <ModalBody>
91
91
  <Stack gap={2}>
92
92
  {#if query}
93
- {#if commandPaletteManager.items.length === 0}
93
+ {#if commandPaletteManager.results.length === 0}
94
94
  <Text>{t('search_no_results', translations)}</Text>
95
95
  {/if}
96
96
  {:else}
97
97
  <Text>{t('command_palette_prompt_default', translations)}</Text>
98
98
  {/if}
99
99
 
100
- {#if commandPaletteManager.items.length > 0}
101
- <div class="flex flex-col">
102
- {#each commandPaletteManager.items as item, i (item.id)}
103
- <CommandPaletteItem {item} selected={selectedIndex === i} onSelect={() => onClose(item)} />
104
- {/each}
105
- </div>
106
- {/if}
100
+ {#each commandPaletteManager.results as result, groupIndex (result.provider.name ?? groupIndex)}
101
+ {#if result.provider.name}
102
+ <Heading size="tiny" class="pt-2">{result.provider.name}</Heading>
103
+ {/if}
104
+ {#if commandPaletteManager.results.length > 0}
105
+ <div class="flex flex-col">
106
+ {#each result.items as item (item.id)}
107
+ <CommandPaletteItem
108
+ {item}
109
+ selected={commandPaletteManager.isSelected(item)}
110
+ onSelect={() => onClose(item)}
111
+ />
112
+ {/each}
113
+ </div>
114
+ {/if}
115
+ {/each}
107
116
  </Stack>
108
117
  </ModalBody>
109
118
  <ModalFooter>
110
119
  <div class="flex w-full justify-around">
111
- {#if !query && commandPaletteManager.items.length === 0}
120
+ {#if !query && commandPaletteManager.results.length === 0}
112
121
  <div class="flex place-items-center gap-1">
113
122
  <span class="flex gap-1 rounded bg-gray-300 p-1 dark:bg-gray-500">
114
123
  <Icon icon={mdiArrowDown} size="1rem" />
@@ -1,20 +1,26 @@
1
1
  import type { ActionItem, MaybePromise, TranslationProps } from '../types.js';
2
2
  export type CommandPaletteTranslations = TranslationProps<'search_placeholder' | 'search_no_results' | 'command_palette_prompt_default' | 'command_palette_to_select' | 'command_palette_to_close' | 'command_palette_to_navigate' | 'command_palette_to_show_all'>;
3
3
  export type ActionProvider = {
4
- name: string;
4
+ name?: string;
5
5
  onSearch: (query?: string) => MaybePromise<ActionItem[]>;
6
6
  };
7
7
  export declare const defaultProvider: ({ name, actions }: {
8
- name: string;
8
+ name?: string;
9
9
  actions: ActionItem[];
10
10
  }) => {
11
- name: string;
11
+ name: string | undefined;
12
12
  onSearch: (query?: string) => ActionItem[];
13
13
  };
14
14
  declare class CommandPaletteManager {
15
15
  #private;
16
16
  get isEnabled(): boolean;
17
- get items(): ({
17
+ get results(): {
18
+ provider: ActionProvider;
19
+ items: Array<ActionItem & {
20
+ id: string;
21
+ }>;
22
+ }[];
23
+ get selectedItem(): {
18
24
  title: string;
19
25
  description?: string;
20
26
  type?: string;
@@ -30,11 +36,16 @@ declare class CommandPaletteManager {
30
36
  };
31
37
  } & import("../types.js").IfLike & {
32
38
  id: string;
33
- })[];
39
+ };
40
+ isSelected(item: {
41
+ id: string;
42
+ }): boolean;
34
43
  enable(): void;
35
44
  setTranslations(translations?: CommandPaletteTranslations): void;
36
45
  queryUpdate(query: string): void;
37
46
  open(initialQuery?: string): void;
47
+ navigateUp(): void;
48
+ navigateDown(): void;
38
49
  loadAllItems(): void;
39
50
  addProvider(provider: ActionProvider): () => void;
40
51
  }
@@ -14,12 +14,21 @@ class CommandPaletteManager {
14
14
  #providers = [];
15
15
  #isEnabled = false;
16
16
  #isOpen = false;
17
- #items = $state([]);
17
+ #results = $state([]);
18
+ #selectedGroupIndex = $state(0);
19
+ #selectedItemIndex = $state(0);
18
20
  get isEnabled() {
19
21
  return this.#isEnabled;
20
22
  }
21
- get items() {
22
- return this.#items;
23
+ get results() {
24
+ return this.#results;
25
+ }
26
+ get selectedItem() {
27
+ const group = this.#results[this.#selectedGroupIndex];
28
+ return group?.items[this.#selectedItemIndex];
29
+ }
30
+ isSelected(item) {
31
+ return this.selectedItem?.id === item.id;
23
32
  }
24
33
  enable() {
25
34
  if (this.#isEnabled) {
@@ -39,15 +48,20 @@ class CommandPaletteManager {
39
48
  this.#translations = translations;
40
49
  }
41
50
  async #onSearch(query) {
42
- const newItems = await Promise.all(this.#providers.map((provider) => Promise.resolve(provider.onSearch(query))));
43
- this.#items = newItems
44
- .flat()
45
- .filter((item) => isEnabled(item))
46
- .map((item) => ({ ...item, id: generateId() }));
51
+ const newResults = await Promise.all(this.#providers.map(async (provider) => {
52
+ const items = await provider.onSearch(query);
53
+ return {
54
+ provider,
55
+ items: items.filter((item) => isEnabled(item)).map((item) => ({ ...item, id: generateId() })),
56
+ };
57
+ }));
58
+ this.#selectedGroupIndex = 0;
59
+ this.#selectedItemIndex = 0;
60
+ this.#results = newResults.filter((result) => result.items.length > 0);
47
61
  }
48
62
  queryUpdate(query) {
49
63
  if (!query) {
50
- this.#items = [];
64
+ this.#results = [];
51
65
  return;
52
66
  }
53
67
  void this.#onSearch(query);
@@ -78,7 +92,7 @@ class CommandPaletteManager {
78
92
  async #onClose(action) {
79
93
  await action?.onAction(action);
80
94
  this.#isOpen = false;
81
- this.#items = [];
95
+ this.#results = [];
82
96
  }
83
97
  open(initialQuery) {
84
98
  if (this.#isOpen) {
@@ -91,6 +105,35 @@ class CommandPaletteManager {
91
105
  this.#isOpen = true;
92
106
  void onClose.then((action) => this.#onClose(action));
93
107
  }
108
+ navigateUp() {
109
+ const groups = this.#results;
110
+ if (groups.length === 0) {
111
+ return;
112
+ }
113
+ this.#selectedItemIndex--;
114
+ if (this.#selectedItemIndex < 0) {
115
+ this.#selectedGroupIndex--; // previous group
116
+ if (this.#selectedGroupIndex < 0) {
117
+ this.#selectedGroupIndex = groups.length - 1; // first group
118
+ }
119
+ this.#selectedItemIndex = groups[this.#selectedGroupIndex].items.length - 1;
120
+ }
121
+ }
122
+ navigateDown() {
123
+ const groups = this.#results;
124
+ if (groups.length === 0) {
125
+ return;
126
+ }
127
+ const group = groups[this.#selectedGroupIndex];
128
+ this.#selectedItemIndex++;
129
+ if (this.#selectedItemIndex >= group.items.length) {
130
+ this.#selectedItemIndex = 0;
131
+ this.#selectedGroupIndex++; // next group
132
+ if (this.#selectedGroupIndex >= groups.length) {
133
+ this.#selectedGroupIndex = 0; // first group
134
+ }
135
+ }
136
+ }
94
137
  loadAllItems() {
95
138
  void this.#onSearch();
96
139
  }
@@ -41,7 +41,6 @@ export declare const Constants: {
41
41
  };
42
42
  export declare const siteCommands: {
43
43
  icon: string;
44
- type: string;
45
44
  iconClass: string;
46
45
  title: string;
47
46
  description: string;
@@ -129,7 +129,6 @@ export const siteCommands = [
129
129
  },
130
130
  ].map((site) => ({
131
131
  icon: mdiOpenInNew,
132
- type: 'Link',
133
132
  iconClass: 'text-indigo-700 dark:text-indigo-200',
134
133
  title: site.title,
135
134
  description: site.description,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.64.1",
3
+ "version": "0.65.1",
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.2.1"
59
59
  },
60
60
  "volta": {
61
- "node": "24.13.1"
61
+ "node": "24.14.0"
62
62
  },
63
63
  "scripts": {
64
64
  "create": "node scripts/create.js",