@immich/ui 0.51.0 → 0.52.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 (30) hide show
  1. package/dist/components/Badge/Badge.svelte +5 -5
  2. package/dist/components/CommandPalette/CommandPaletteDefaultProvider.svelte +14 -0
  3. package/dist/components/CommandPalette/CommandPaletteDefaultProvider.svelte.d.ts +8 -0
  4. package/dist/components/CommandPalette/CommandPaletteItem.svelte +2 -21
  5. package/dist/components/CommandPalette/CommandPaletteItem.svelte.d.ts +0 -1
  6. package/dist/components/CommandPalette/CommandPaletteProvider.svelte +18 -0
  7. package/dist/components/CommandPalette/CommandPaletteProvider.svelte.d.ts +7 -0
  8. package/dist/components/Heading/Heading.svelte +6 -13
  9. package/dist/components/Markdown/Heading.svelte +9 -12
  10. package/dist/components/Modal/Modal.svelte +1 -4
  11. package/dist/components/Textarea/Textarea.svelte +8 -2
  12. package/dist/components/Toast/ToastPanel.svelte +19 -16
  13. package/dist/index.d.ts +2 -1
  14. package/dist/index.js +2 -1
  15. package/dist/internal/Button.svelte +10 -1
  16. package/dist/internal/CommandPaletteModal.svelte +26 -23
  17. package/dist/internal/CommandPaletteModal.svelte.d.ts +3 -1
  18. package/dist/services/command-palette-manager.svelte.d.ts +19 -77
  19. package/dist/services/command-palette-manager.svelte.js +44 -152
  20. package/dist/site/SiteFooter.svelte +3 -0
  21. package/dist/site/constants.d.ts +1 -0
  22. package/dist/site/constants.js +2 -1
  23. package/dist/types.d.ts +1 -0
  24. package/dist/utilities/common.d.ts +1 -0
  25. package/dist/utilities/common.js +7 -0
  26. package/dist/utilities/internal.d.ts +3 -1
  27. package/dist/utilities/internal.js +3 -0
  28. package/package.json +1 -1
  29. package/dist/components/CommandPalette/CommandPaletteContext.svelte +0 -18
  30. package/dist/components/CommandPalette/CommandPaletteContext.svelte.d.ts +0 -7
@@ -37,11 +37,11 @@
37
37
  },
38
38
  textSize: styleVariants.textSize,
39
39
  paddingSize: {
40
- tiny: 'px-3 py-1',
41
- small: 'px-3.25 py-1.25',
42
- medium: 'px-3.75 py-1.5',
43
- large: 'px-4 py-1.75',
44
- giant: 'px-4.25 py-2',
40
+ tiny: 'px-1.5 py-1',
41
+ small: 'px-1.75 py-0.75',
42
+ medium: 'px-2.5 py-1',
43
+ large: 'px-2.75 py-1',
44
+ giant: 'px-3 py-1.25',
45
45
  },
46
46
  roundedSize: {
47
47
  tiny: 'rounded-lg',
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import CommandPaletteProvider from './CommandPaletteProvider.svelte';
3
+ import { defaultProvider } from '../../services/command-palette-manager.svelte.js';
4
+ import type { ActionItem } from '../../types.js';
5
+
6
+ type Props = {
7
+ name: string;
8
+ actions?: ActionItem[];
9
+ };
10
+
11
+ const { name, actions = [] }: Props = $props();
12
+ </script>
13
+
14
+ <CommandPaletteProvider providers={[defaultProvider({ name, actions })]} />
@@ -0,0 +1,8 @@
1
+ import type { ActionItem } from '../../types.js';
2
+ type Props = {
3
+ name: string;
4
+ actions?: ActionItem[];
5
+ };
6
+ declare const CommandPaletteDefaultProvider: import("svelte").Component<Props, {}, "">;
7
+ type CommandPaletteDefaultProvider = ReturnType<typeof CommandPaletteDefaultProvider>;
8
+ export default CommandPaletteDefaultProvider;
@@ -3,26 +3,18 @@
3
3
  import Badge from '../Badge/Badge.svelte';
4
4
  import Button from '../Button/Button.svelte';
5
5
  import Icon from '../Icon/Icon.svelte';
6
- import IconButton from '../IconButton/IconButton.svelte';
7
6
  import Kbd from '../Kbd/Kbd.svelte';
8
7
  import Text from '../Text/Text.svelte';
9
8
  import type { ActionItem } from '../../types.js';
10
9
  import { cleanClass } from '../../utilities/internal.js';
11
- import { mdiClose } from '@mdi/js';
12
10
 
13
11
  type Props = {
14
12
  item: ActionItem;
15
13
  selected: boolean;
16
14
  onSelect: () => void;
17
- onRemove?: () => void;
18
15
  };
19
16
 
20
- const { item, selected, onRemove, onSelect }: Props = $props();
21
-
22
- const handleRemove = (event: MouseEvent) => {
23
- event.stopPropagation();
24
- onRemove?.();
25
- };
17
+ const { item, selected, onSelect }: Props = $props();
26
18
 
27
19
  const shortcuts =
28
20
  item.shortcuts === undefined ? [] : Array.isArray(item.shortcuts) ? item.shortcuts : [item.shortcuts];
@@ -66,18 +58,7 @@
66
58
  {/if}
67
59
  </div>
68
60
  </div>
69
- {#if onRemove}
70
- <IconButton
71
- size="small"
72
- onclick={handleRemove}
73
- class="shrink-0"
74
- icon={mdiClose}
75
- shape="round"
76
- variant="ghost"
77
- color="secondary"
78
- aria-label="Remove"
79
- />
80
- {:else if renderedShortcuts.length > 0}
61
+ {#if renderedShortcuts.length > 0}
81
62
  <div class="flex shrink-0 flex-col justify-end gap-1">
82
63
  {#each renderedShortcuts as shortcut (shortcut.join('-'))}
83
64
  <div class="flex justify-end">
@@ -3,7 +3,6 @@ type Props = {
3
3
  item: ActionItem;
4
4
  selected: boolean;
5
5
  onSelect: () => void;
6
- onRemove?: () => void;
7
6
  };
8
7
  declare const CommandPaletteItem: import("svelte").Component<Props, {}, "">;
9
8
  type CommandPaletteItem = ReturnType<typeof CommandPaletteItem>;
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { commandPaletteManager, type ActionProvider } from '../../services/command-palette-manager.svelte.js';
3
+
4
+ type Props = {
5
+ providers: ActionProvider[];
6
+ };
7
+
8
+ const { providers }: Props = $props();
9
+
10
+ $effect(() => {
11
+ const callbacks: Array<() => void> = [];
12
+ for (const provider of providers) {
13
+ callbacks.push(commandPaletteManager.addProvider(provider));
14
+ }
15
+
16
+ return () => callbacks.forEach((callback) => callback());
17
+ });
18
+ </script>
@@ -0,0 +1,7 @@
1
+ import { type ActionProvider } from '../../services/command-palette-manager.svelte.js';
2
+ type Props = {
3
+ providers: ActionProvider[];
4
+ };
5
+ declare const CommandPaletteProvider: import("svelte").Component<Props, {}, "">;
6
+ type CommandPaletteProvider = ReturnType<typeof CommandPaletteProvider>;
7
+ export default CommandPaletteProvider;
@@ -20,25 +20,18 @@
20
20
  children: Snippet;
21
21
  } & HTMLAttributes<HTMLElement>;
22
22
 
23
- const {
24
- tag = 'p',
25
- size = 'medium',
26
- fontWeight = 'semi-bold',
27
- class: className,
28
- children,
29
- ...restProps
30
- }: Props = $props();
23
+ const { tag = 'p', size = 'medium', fontWeight = 'bold', class: className, children, ...restProps }: Props = $props();
31
24
 
32
25
  const styles = tv({
33
26
  base: 'leading-none tracking-tight',
34
27
  variants: {
35
28
  size: {
36
- tiny: 'text-lg',
37
- small: 'text-xl',
38
- medium: 'text-2xl',
29
+ tiny: 'text-base',
30
+ small: 'text-lg',
31
+ medium: 'text-xl',
39
32
  large: 'text-3xl',
40
- giant: 'text-4xl',
41
- title: 'text-5xl',
33
+ giant: 'text-5xl',
34
+ title: 'text-7xl',
42
35
  },
43
36
  },
44
37
  });
@@ -11,33 +11,30 @@
11
11
 
12
12
  const { children, id, level }: Props = $props();
13
13
 
14
- const getSizeAndTag = (level?: number): { size: Size; tag: HeadingTag } => {
14
+ const getSizeAndTag = (level?: number): { size: Size; tag: HeadingTag; padding: string } => {
15
15
  switch (level) {
16
16
  case 1: {
17
- return { size: 'giant', tag: 'h1' };
17
+ return { size: 'giant', tag: 'h1', padding: 'mb-6' };
18
18
  }
19
19
  case 2: {
20
- return { size: 'large', tag: 'h2' };
20
+ return { size: 'large', tag: 'h2', padding: 'my-4' };
21
21
  }
22
22
  case 3: {
23
- return { size: 'medium', tag: 'h3' };
23
+ return { size: 'medium', tag: 'h3', padding: 'my-3' };
24
24
  }
25
25
  case 4: {
26
- return { size: 'small', tag: 'h4' };
26
+ return { size: 'small', tag: 'h4', padding: 'my-1' };
27
27
  }
28
28
  case 5: {
29
- return { size: 'tiny', tag: 'h5' };
30
- }
31
- case 6: {
32
- return { size: 'tiny', tag: 'h6' };
29
+ return { size: 'tiny', tag: 'h5', padding: 'my-0.5' };
33
30
  }
34
31
  default: {
35
- return { size: 'tiny', tag: 'p' };
32
+ return { size: 'tiny', tag: 'p', padding: '' };
36
33
  }
37
34
  }
38
35
  };
39
36
 
40
- let { size, tag } = $derived(getSizeAndTag(level));
37
+ let { size, tag, padding } = $derived(getSizeAndTag(level));
41
38
  </script>
42
39
 
43
- <Heading {size} {tag} class="mt-4 mb-1" {id} {children} />
40
+ <Heading {size} {tag} class={padding} {id} {children} />
@@ -10,11 +10,10 @@
10
10
  import Logo from '../Logo/Logo.svelte';
11
11
  import TooltipProvider from '../Tooltip/TooltipProvider.svelte';
12
12
  import { ChildKey, zIndex } from '../../constants.js';
13
- import { commandPaletteManager } from '../../services/command-palette-manager.svelte.js';
14
13
  import type { ModalSize } from '../../types.js';
15
14
  import { cleanClass } from '../../utilities/internal.js';
16
15
  import { Dialog } from 'bits-ui';
17
- import { onMount, tick, type Snippet } from 'svelte';
16
+ import { tick, type Snippet } from 'svelte';
18
17
  import { tv } from 'tailwind-variants';
19
18
 
20
19
  type Props = {
@@ -87,8 +86,6 @@
87
86
 
88
87
  const interactOutsideBehavior = $derived(closeOnBackdropClick ? 'close' : 'ignore');
89
88
  const escapeKeydownBehavior = $derived(closeOnEsc ? 'close' : 'ignore');
90
-
91
- onMount(() => commandPaletteManager.pushContextLayer());
92
89
  </script>
93
90
 
94
91
  <Dialog.Root open={true} onOpenChange={(isOpen: boolean) => !isOpen && handleClose()}>
@@ -5,6 +5,7 @@
5
5
  import { styleVariants } from '../../styles.js';
6
6
  import type { TextareaProps } from '../../types.js';
7
7
  import { cleanClass, generateId } from '../../utilities/internal.js';
8
+ import { onMount } from 'svelte';
8
9
  import type { FormEventHandler } from 'svelte/elements';
9
10
  import { tv } from 'tailwind-variants';
10
11
 
@@ -53,15 +54,20 @@
53
54
  const labelId = `label-${id}`;
54
55
  const descriptionId = $derived(description ? `description-${id}` : undefined);
55
56
 
56
- const onInput: FormEventHandler<HTMLTextAreaElement> = (event) => {
57
- const element = event.target as HTMLTextAreaElement;
57
+ const autogrow = (element: HTMLTextAreaElement | null) => {
58
58
  if (element && grow) {
59
+ element.style.minHeight = '0';
59
60
  element.style.height = 'auto';
60
61
  element.style.height = `${element.scrollHeight}px`;
61
62
  }
63
+ };
62
64
 
65
+ const onInput: FormEventHandler<HTMLTextAreaElement> = (event) => {
66
+ autogrow(event.target as HTMLTextAreaElement);
63
67
  restProps?.oninput?.(event);
64
68
  };
69
+
70
+ onMount(() => autogrow(ref));
65
71
  </script>
66
72
 
67
73
  <div class="flex w-full flex-col gap-1" bind:this={containerRef}>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Toast from './Toast.svelte';
3
+ import TooltipProvider from '../Tooltip/TooltipProvider.svelte';
3
4
  import { zIndex } from '../../constants.js';
4
5
  import { isCustomToast } from '../../services/toast-manager.svelte.js';
5
6
  import type { ToastPanelProps } from '../../types.js';
@@ -10,19 +11,21 @@
10
11
  const isEmpty = $derived(items.length === 0);
11
12
  </script>
12
13
 
13
- <div
14
- class={cleanClass(
15
- isEmpty ? 'hidden' : 'absolute top-0 right-0 flex flex-col items-end justify-end gap-2 p-4',
16
- zIndex.ToastPanel,
17
- className,
18
- )}
19
- {...props}
20
- >
21
- {#each items as item (item.id)}
22
- {#if isCustomToast(item)}
23
- <item.component {...item.props} />
24
- {:else}
25
- <Toast {...item} />
26
- {/if}
27
- {/each}
28
- </div>
14
+ <TooltipProvider>
15
+ <div
16
+ class={cleanClass(
17
+ isEmpty ? 'hidden' : 'absolute top-0 right-0 flex flex-col items-end justify-end gap-2 p-4',
18
+ zIndex.ToastPanel,
19
+ className,
20
+ )}
21
+ {...props}
22
+ >
23
+ {#each items as item (item.id)}
24
+ {#if isCustomToast(item)}
25
+ <item.component {...item.props} />
26
+ {:else}
27
+ <Toast {...item} />
28
+ {/if}
29
+ {/each}
30
+ </div>
31
+ </TooltipProvider>
package/dist/index.d.ts CHANGED
@@ -29,7 +29,8 @@ export { default as Checkbox } from './components/Checkbox/Checkbox.svelte';
29
29
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
30
30
  export { default as Code } from './components/Code/Code.svelte';
31
31
  export { default as CodeBlock } from './components/CodeBlock/CodeBlock.svelte';
32
- export { default as CommandPaletteContext } from './components/CommandPalette/CommandPaletteContext.svelte';
32
+ export { default as CommandPaletteDefaultProvider } from './components/CommandPalette/CommandPaletteDefaultProvider.svelte';
33
+ export { default as CommandPaletteProvider } from './components/CommandPalette/CommandPaletteProvider.svelte';
33
34
  export { default as ConfirmModal } from './components/ConfirmModal/ConfirmModal.svelte';
34
35
  export { default as Container } from './components/Container/Container.svelte';
35
36
  export { default as ContextMenuButton } from './components/ContextMenu/ContextMenuButton.svelte';
package/dist/index.js CHANGED
@@ -31,7 +31,8 @@ export { default as Checkbox } from './components/Checkbox/Checkbox.svelte';
31
31
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
32
32
  export { default as Code } from './components/Code/Code.svelte';
33
33
  export { default as CodeBlock } from './components/CodeBlock/CodeBlock.svelte';
34
- export { default as CommandPaletteContext } from './components/CommandPalette/CommandPaletteContext.svelte';
34
+ export { default as CommandPaletteDefaultProvider } from './components/CommandPalette/CommandPaletteDefaultProvider.svelte';
35
+ export { default as CommandPaletteProvider } from './components/CommandPalette/CommandPaletteProvider.svelte';
35
36
  export { default as ConfirmModal } from './components/ConfirmModal/ConfirmModal.svelte';
36
37
  export { default as Container } from './components/Container/Container.svelte';
37
38
  export { default as ContextMenuButton } from './components/ContextMenu/ContextMenuButton.svelte';
@@ -37,7 +37,7 @@
37
37
  const disabled = $derived((restProps as HTMLButtonAttributes).disabled || loading);
38
38
 
39
39
  const buttonVariants = tv({
40
- base: 'ring-offset-background focus-visible:ring-ring flex items-center justify-center gap-1 rounded-md text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
40
+ base: 'flex items-center justify-center gap-1 rounded-md text-sm font-medium outline-offset-2 transition-colors focus-visible:outline-2',
41
41
  variants: {
42
42
  disabled: {
43
43
  true: 'disabled:pointer-events-none disabled:opacity-50 aria-disabled:opacity-50',
@@ -69,6 +69,14 @@
69
69
  large: 'rounded-xl',
70
70
  giant: 'rounded-2xl',
71
71
  },
72
+ focusOutlineColor: {
73
+ primary: 'outline-primary',
74
+ secondary: 'outline-dark',
75
+ success: 'outline-success',
76
+ danger: 'outline-danger',
77
+ warning: 'outline-warning',
78
+ info: 'outline-info',
79
+ },
72
80
  filledColor: styleVariants.filledColor,
73
81
  filledColorHover: styleVariants.filledColorHover,
74
82
  outlineColor: {
@@ -112,6 +120,7 @@
112
120
  filledColorHover: variant === 'filled' ? color : undefined,
113
121
  outlineColor: variant === 'outline' ? color : undefined,
114
122
  ghostColor: variant === 'ghost' ? color : undefined,
123
+ focusOutlineColor: color,
115
124
  }),
116
125
  className,
117
126
  ),
@@ -15,13 +15,17 @@
15
15
  type CommandPaletteTranslations,
16
16
  } from '../services/command-palette-manager.svelte.js';
17
17
  import { t } from '../services/translation.svelte.js';
18
+ import type { ActionItem } from '../types.js';
18
19
  import { mdiArrowDown, mdiArrowUp, mdiKeyboardEsc, mdiKeyboardReturn, mdiMagnify } from '@mdi/js';
19
20
 
20
21
  type Props = {
21
- onClose: () => void;
22
+ onClose: (action?: ActionItem) => void;
22
23
  translations?: CommandPaletteTranslations;
24
+ initialQuery?: string;
23
25
  };
24
26
 
27
+ const { onClose, translations, initialQuery = '' }: Props = $props();
28
+
25
29
  const handleUp = (event: KeyboardEvent) => handleNavigate(event, 'up');
26
30
  const handleDown = (event: KeyboardEvent) => handleNavigate(event, 'down');
27
31
  const handleSelect = (event: KeyboardEvent) => handleNavigate(event, 'select');
@@ -30,23 +34,31 @@
30
34
 
31
35
  switch (direction) {
32
36
  case 'up': {
33
- commandPaletteManager.up();
37
+ selectedIndex = Math.max((selectedIndex === 0 ? commandPaletteManager.items.length : selectedIndex) - 1, 0);
34
38
  break;
35
39
  }
36
40
 
37
41
  case 'down': {
38
- commandPaletteManager.down();
42
+ if (!query && commandPaletteManager.items.length === 0) {
43
+ commandPaletteManager.loadAllItems();
44
+ break;
45
+ }
46
+
47
+ selectedIndex = (selectedIndex + 1) % commandPaletteManager.items.length || 0;
39
48
  break;
40
49
  }
41
50
 
42
51
  case 'select': {
43
- await commandPaletteManager.select();
52
+ onClose(commandPaletteManager.items[selectedIndex]);
44
53
  break;
45
54
  }
46
55
  }
47
56
  };
48
57
 
49
- const { onClose, translations }: Props = $props();
58
+ let selectedIndex = $state(0);
59
+ let query = $state(initialQuery);
60
+
61
+ $effect(() => commandPaletteManager.queryUpdate(query));
50
62
  </script>
51
63
 
52
64
  <svelte:window
@@ -61,43 +73,34 @@
61
73
  ]}
62
74
  />
63
75
 
64
- <Modal size="large" {onClose} closeOnBackdropClick class="max-h-[75vh] lg:max-h-[50vh]">
76
+ <Modal size="large" {onClose} closeOnBackdropClick class="max-h-[85vh] lg:max-h-[75vh]">
65
77
  <ModalHeader>
66
78
  <div class="flex place-items-center gap-1">
67
79
  <Input
68
- bind:value={commandPaletteManager.query}
80
+ bind:value={query}
69
81
  placeholder={t('search_placeholder', translations)}
70
82
  leadingIcon={mdiMagnify}
71
83
  tabindex={1}
72
84
  />
73
85
  <div>
74
- <CloseButton onclick={() => commandPaletteManager.close()} class="md:hidden" />
86
+ <CloseButton onclick={() => onClose()} class="md:hidden" />
75
87
  </div>
76
88
  </div>
77
89
  </ModalHeader>
78
90
  <ModalBody>
79
91
  <Stack gap={2}>
80
- {#if commandPaletteManager.query}
81
- {#if commandPaletteManager.results.length === 0}
92
+ {#if query}
93
+ {#if commandPaletteManager.items.length === 0}
82
94
  <Text>{t('search_no_results', translations)}</Text>
83
95
  {/if}
84
- {:else if commandPaletteManager.recentItems.length > 0}
85
- <Text>{t('search_recently_used', translations)}</Text>
86
96
  {:else}
87
97
  <Text>{t('command_palette_prompt_default', translations)}</Text>
88
98
  {/if}
89
99
 
90
- {#if commandPaletteManager.results.length > 0}
100
+ {#if commandPaletteManager.items.length > 0}
91
101
  <div class="flex flex-col">
92
- {#each commandPaletteManager.results as item, i (item.id)}
93
- <CommandPaletteItem
94
- {item}
95
- selected={commandPaletteManager.selectedIndex === i}
96
- onRemove={commandPaletteManager.query || commandPaletteManager.isShowAll
97
- ? undefined
98
- : () => commandPaletteManager.remove(i)}
99
- onSelect={() => commandPaletteManager.select(i)}
100
- />
102
+ {#each commandPaletteManager.items as item, i (item.id)}
103
+ <CommandPaletteItem {item} selected={selectedIndex === i} onSelect={() => onClose(item)} />
101
104
  {/each}
102
105
  </div>
103
106
  {/if}
@@ -105,7 +108,7 @@
105
108
  </ModalBody>
106
109
  <ModalFooter>
107
110
  <div class="flex w-full justify-around">
108
- {#if commandPaletteManager.isEmpty}
111
+ {#if !query && commandPaletteManager.items.length === 0}
109
112
  <div class="flex place-items-center gap-1">
110
113
  <span class="flex gap-1 rounded bg-gray-300 p-1 dark:bg-gray-500">
111
114
  <Icon icon={mdiArrowDown} size="1rem" />
@@ -1,7 +1,9 @@
1
1
  import { type CommandPaletteTranslations } from '../services/command-palette-manager.svelte.js';
2
+ import type { ActionItem } from '../types.js';
2
3
  type Props = {
3
- onClose: () => void;
4
+ onClose: (action?: ActionItem) => void;
4
5
  translations?: CommandPaletteTranslations;
6
+ initialQuery?: string;
5
7
  };
6
8
  declare const CommandPaletteModal: import("svelte").Component<Props, {}, "">;
7
9
  type CommandPaletteModal = ReturnType<typeof CommandPaletteModal>;
@@ -1,64 +1,20 @@
1
- import type { ActionItem, MaybeArray, TranslationProps } from '../types.js';
1
+ import type { ActionItem, MaybePromise, TranslationProps } from '../types.js';
2
2
  export type CommandPaletteTranslations = TranslationProps<'search_placeholder' | 'search_no_results' | 'search_recently_used' | 'command_palette_prompt_default' | 'command_palette_to_select' | 'command_palette_to_close' | 'command_palette_to_navigate' | 'command_palette_to_show_all' | 'global'>;
3
- export declare const asText: (...items: unknown[]) => string;
3
+ export type ActionProvider = {
4
+ name: string;
5
+ onSearch: (query?: string) => MaybePromise<ActionItem[]>;
6
+ };
7
+ export declare const defaultProvider: ({ name, actions }: {
8
+ name: string;
9
+ actions: ActionItem[];
10
+ }) => {
11
+ name: string;
12
+ onSearch: (query?: string) => ActionItem[];
13
+ };
4
14
  declare class CommandPaletteManager {
5
15
  #private;
6
- selectedIndex: number;
7
- items: ({
8
- title: string;
9
- description?: string;
10
- type?: string;
11
- searchText?: string;
12
- icon: import("../types.js").IconLike;
13
- iconClass?: string;
14
- color?: import("../types.js").Color;
15
- onAction: import("../types.js").ActionItemHandler;
16
- shortcuts?: MaybeArray<import("../actions/shortcut.js").Shortcut>;
17
- shortcutOptions?: {
18
- ignoreInputFields?: boolean;
19
- preventDefault?: boolean;
20
- };
21
- isGlobal?: boolean;
22
- } & import("../types.js").IfLike & {
23
- id: string;
24
- })[];
25
- filteredItems: ({
26
- title: string;
27
- description?: string;
28
- type?: string;
29
- searchText?: string;
30
- icon: import("../types.js").IconLike;
31
- iconClass?: string;
32
- color?: import("../types.js").Color;
33
- onAction: import("../types.js").ActionItemHandler;
34
- shortcuts?: MaybeArray<import("../actions/shortcut.js").Shortcut>;
35
- shortcutOptions?: {
36
- ignoreInputFields?: boolean;
37
- preventDefault?: boolean;
38
- };
39
- isGlobal?: boolean;
40
- } & import("../types.js").IfLike & {
41
- id: string;
42
- })[];
43
- recentItems: ({
44
- title: string;
45
- description?: string;
46
- type?: string;
47
- searchText?: string;
48
- icon: import("../types.js").IconLike;
49
- iconClass?: string;
50
- color?: import("../types.js").Color;
51
- onAction: import("../types.js").ActionItemHandler;
52
- shortcuts?: MaybeArray<import("../actions/shortcut.js").Shortcut>;
53
- shortcutOptions?: {
54
- ignoreInputFields?: boolean;
55
- preventDefault?: boolean;
56
- };
57
- isGlobal?: boolean;
58
- } & import("../types.js").IfLike & {
59
- id: string;
60
- })[];
61
- results: ({
16
+ get isEnabled(): boolean;
17
+ get items(): ({
62
18
  title: string;
63
19
  description?: string;
64
20
  type?: string;
@@ -67,7 +23,7 @@ declare class CommandPaletteManager {
67
23
  iconClass?: string;
68
24
  color?: import("../types.js").Color;
69
25
  onAction: import("../types.js").ActionItemHandler;
70
- shortcuts?: MaybeArray<import("../actions/shortcut.js").Shortcut>;
26
+ shortcuts?: import("../types.js").MaybeArray<import("../actions/shortcut.js").Shortcut>;
71
27
  shortcutOptions?: {
72
28
  ignoreInputFields?: boolean;
73
29
  preventDefault?: boolean;
@@ -76,26 +32,12 @@ declare class CommandPaletteManager {
76
32
  } & import("../types.js").IfLike & {
77
33
  id: string;
78
34
  })[];
79
- get isEnabled(): boolean;
80
35
  enable(): void;
81
- get query(): string;
82
- set query(query: string);
83
- get isShowAll(): boolean;
84
- get isEmpty(): boolean;
85
36
  setTranslations(translations?: CommandPaletteTranslations): void;
86
- pushContextLayer(): (() => void) | undefined;
87
- popContextLayer(): void;
88
- open(): void;
89
- close(): Promise<void> | undefined;
90
- select(selectedIndex?: number): Promise<void>;
91
- remove(index: number): void;
92
- up(): void;
93
- down(): void;
94
- reset(): void;
95
- addCommands(itemOrItems: MaybeArray<ActionItem>): () => void;
96
- removeCommands(itemOrItems: MaybeArray<{
97
- id: string;
98
- }>): void;
37
+ queryUpdate(query: string): void;
38
+ open(initialQuery?: string): void;
39
+ loadAllItems(): void;
40
+ addProvider(provider: ActionProvider): () => void;
99
41
  }
100
42
  export declare const commandPaletteManager: CommandPaletteManager;
101
43
  export {};
@@ -1,39 +1,23 @@
1
- import { matchesShortcut, shortcuts, shouldIgnoreEvent } from '../actions/shortcut.js';
1
+ import { matchesShortcut, shortcuts } from '../actions/shortcut.js';
2
2
  import CommandPaletteModal from '../internal/CommandPaletteModal.svelte';
3
- import { generateId, isEnabled } from '../utilities/internal.js';
4
3
  import { modalManager } from './modal-manager.svelte.js';
5
- export const asText = (...items) => {
6
- return items
7
- .filter((item) => item !== undefined && item !== null)
8
- .map((items) => String(items))
9
- .join('|')
10
- .toLowerCase();
11
- };
12
- const isMatch = ({ title, description, type, searchText = asText(title, description, type) }, query) => {
13
- if (!query) {
14
- return true;
15
- }
16
- return searchText.includes(query);
17
- };
4
+ import { asArray, generateId, getSearchString } from '../utilities/internal.js';
5
+ export const defaultProvider = ({ name, actions }) => ({
6
+ name,
7
+ onSearch: (query) => query ? actions.filter((action) => getSearchString(action).includes(query.toLowerCase())) : actions,
8
+ });
18
9
  class CommandPaletteManager {
19
- #query = $state('');
20
- selectedIndex = $state(0);
21
- #isEnabled = $state(false);
22
- #normalizedQuery = $derived(this.#query.toLowerCase());
23
- #modal;
24
10
  #translations = {};
11
+ #providers = [];
12
+ #isEnabled = false;
25
13
  #isOpen = false;
26
- #isShowAll = $state(false);
27
- #globalLayer = $state({ items: [], recentItems: [] });
28
- #layers = $state([{ items: [], recentItems: [] }]);
29
- items = $derived([...this.#globalLayer.items, ...this.#layers.at(-1).items].filter(isEnabled));
30
- filteredItems = $derived(this.items.filter((item) => isMatch(item, this.#normalizedQuery)).slice(0, 100));
31
- recentItems = $derived([...this.#globalLayer.recentItems, ...this.#layers.at(-1).recentItems].filter(isEnabled));
32
- results = $derived(this.#isShowAll ? this.items : this.#query ? this.filteredItems : this.recentItems);
33
- #isEmpty = $derived(!this.#isShowAll && !this.#query && this.results.length === 0);
14
+ #items = $state([]);
34
15
  get isEnabled() {
35
16
  return this.#isEnabled;
36
17
  }
18
+ get items() {
19
+ return this.#items;
20
+ }
37
21
  enable() {
38
22
  if (this.#isEnabled) {
39
23
  return;
@@ -48,146 +32,54 @@ class CommandPaletteManager {
48
32
  document.body.addEventListener('keydown', (event) => this.#handleKeydown(event));
49
33
  }
50
34
  }
51
- get query() {
52
- return this.#query;
53
- }
54
- set query(query) {
55
- this.#query = query;
56
- if (this.#isShowAll && query) {
57
- this.#isShowAll = false;
58
- }
59
- }
60
- get isShowAll() {
61
- return this.#isShowAll;
62
- }
63
- get isEmpty() {
64
- return this.#isEmpty;
65
- }
66
- async #handleKeydown(event) {
67
- const command = this.items.find(({ shortcuts }) => {
68
- if (!shortcuts) {
69
- return;
70
- }
71
- if (shortcuts)
72
- return Array.isArray(shortcuts)
73
- ? shortcuts.some((shortcut) => matchesShortcut(event, shortcut))
74
- : matchesShortcut(event, shortcuts);
75
- });
76
- if (!command) {
77
- return;
78
- }
79
- const { ignoreInputFields = true, preventDefault = true } = command.shortcutOptions ?? {};
80
- if (ignoreInputFields && shouldIgnoreEvent(event)) {
81
- return;
82
- }
83
- if (preventDefault) {
84
- event.preventDefault();
85
- }
86
- await command.onAction(command);
87
- }
88
35
  setTranslations(translations = {}) {
89
36
  this.#translations = translations;
90
37
  }
91
- pushContextLayer() {
92
- if (!this.#isEnabled) {
93
- return;
94
- }
95
- // we do not want the command palette to have its own context layer
96
- if (this.#isOpen) {
97
- return;
98
- }
99
- this.#layers.push({ items: [], recentItems: [] });
100
- return () => this.popContextLayer();
101
- }
102
- popContextLayer() {
103
- if (this.#layers.length > 1) {
104
- this.#layers = this.#layers.slice(0, -1);
105
- }
38
+ async #onSearch(query) {
39
+ const newItems = await Promise.all(this.#providers.map((provider) => Promise.resolve(provider.onSearch(query))));
40
+ this.#items = newItems.flat().map((item) => ({ ...item, id: generateId() }));
106
41
  }
107
- open() {
108
- if (this.#modal || !this.#isEnabled) {
42
+ queryUpdate(query) {
43
+ if (!query) {
44
+ this.#items = [];
109
45
  return;
110
46
  }
111
- this.selectedIndex = 0;
112
- this.#isOpen = true;
113
- const { close, onClose } = modalManager.open(CommandPaletteModal, { translations: this.#translations });
114
- this.#modal = { close };
115
- void onClose.then(() => this.#onClose());
47
+ void this.#onSearch(query);
116
48
  }
117
- close() {
118
- if (!this.#modal) {
119
- return;
49
+ async #handleKeydown(event) {
50
+ const actions = await Promise.all(this.#providers.map((provider) => Promise.resolve(provider.onSearch())));
51
+ for (const action of actions.flat()) {
52
+ const shortcuts = asArray(action.shortcuts);
53
+ if (shortcuts.some((shortcut) => matchesShortcut(event, shortcut))) {
54
+ action?.onAction(action);
55
+ }
120
56
  }
121
- return this.#modal.close();
122
57
  }
123
- #onClose() {
124
- this.#query = '';
125
- this.#modal = undefined;
58
+ async #onClose(action) {
59
+ await action?.onAction(action);
126
60
  this.#isOpen = false;
127
- this.#isShowAll = false;
128
- }
129
- async select(selectedIndex) {
130
- const selected = this.results[selectedIndex ?? this.selectedIndex];
131
- if (!selected) {
132
- return;
133
- }
134
- if (selected.isGlobal) {
135
- this.#globalLayer.recentItems = this.#globalLayer.recentItems.filter(({ id }) => id !== selected.id);
136
- this.#globalLayer.recentItems.unshift(selected);
137
- }
138
- else {
139
- this.#layers.at(-1).recentItems = this.#layers.at(-1).recentItems.filter(({ id }) => id !== selected.id);
140
- this.#layers.at(-1)?.recentItems.unshift(selected);
141
- }
142
- await selected.onAction(selected);
143
- await this.close();
144
- }
145
- remove(index) {
146
- const item = this.recentItems.at(index);
147
- if (!item) {
148
- return;
149
- }
150
- this.#globalLayer.recentItems = this.#globalLayer.recentItems.filter(({ id }) => id !== item.id);
151
- this.#layers.at(-1).recentItems = this.#layers.at(-1).recentItems.filter(({ id }) => id !== item.id);
61
+ this.#items = [];
152
62
  }
153
- up() {
154
- this.selectedIndex = (this.selectedIndex - 1 + this.results.length) % (this.results.length || 1);
155
- }
156
- down() {
157
- if (this.#isEmpty) {
158
- this.#isShowAll = true;
63
+ open(initialQuery) {
64
+ if (this.#isOpen) {
159
65
  return;
160
66
  }
161
- this.selectedIndex = (this.selectedIndex + 1) % (this.results.length || 1);
162
- }
163
- reset() {
164
- this.#layers = [{ items: [], recentItems: [] }];
165
- this.#globalLayer = { items: [], recentItems: [] };
166
- this.#query = '';
67
+ const { onClose } = modalManager.open(CommandPaletteModal, {
68
+ translations: this.#translations,
69
+ initialQuery,
70
+ });
71
+ this.#isOpen = true;
72
+ void onClose.then((action) => this.#onClose(action));
167
73
  }
168
- addCommands(itemOrItems) {
169
- const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
170
- const itemsWithId = items.map((item) => ({
171
- ...item,
172
- id: generateId(),
173
- }));
174
- const globalItems = itemsWithId.filter(({ isGlobal }) => isGlobal);
175
- const localItems = itemsWithId.filter(({ isGlobal }) => !isGlobal);
176
- this.#globalLayer.items.push(...globalItems);
177
- this.#layers.at(-1).items.push(...localItems);
178
- return () => this.removeCommands(itemsWithId);
74
+ loadAllItems() {
75
+ void this.#onSearch();
179
76
  }
180
- #removeCommands(layer, ids) {
181
- return {
182
- items: layer.items.filter(({ id }) => !ids[id]),
183
- recentItems: layer.recentItems.filter(({ id }) => !ids[id]),
184
- };
77
+ addProvider(provider) {
78
+ this.#providers.push(provider);
79
+ return () => this.#removeProvider(provider);
185
80
  }
186
- removeCommands(itemOrItems) {
187
- const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
188
- const ids = items.reduce((acc, { id }) => ({ ...acc, [id]: true }), {});
189
- this.#layers = this.#layers.map((layer) => this.#removeCommands(layer, ids));
190
- this.#globalLayer = this.#removeCommands(this.#globalLayer, ids);
81
+ #removeProvider(provider) {
82
+ this.#providers = this.#providers.filter((actionProvider) => actionProvider !== provider);
191
83
  }
192
84
  }
193
85
  export const commandPaletteManager = new CommandPaletteManager();
@@ -31,6 +31,9 @@
31
31
  <SiteFooterLink href={Constants.Sites.Docs} text="Documentation" />
32
32
  <SiteFooterLink href={Constants.Sites.My} text="My Immich" />
33
33
  <SiteFooterLink href={Constants.Sites.Api} text="Immich API" />
34
+ <SiteFooterLink href={Constants.Sites.Data} text="Immich Data" />
35
+ <SiteFooterLink href={Constants.Sites.Datasets} text="Immich Datasets" />
36
+ <SiteFooterLink href={Constants.Sites.Awesome} text="Awesome Immich" />
34
37
  </Stack>
35
38
 
36
39
  <Stack>
@@ -26,6 +26,7 @@ export declare const Constants: {
26
26
  Get: string;
27
27
  My: string;
28
28
  Store: string;
29
+ Awesome: string;
29
30
  Ui: string;
30
31
  };
31
32
  Pages: {
@@ -1,5 +1,5 @@
1
1
  import { goto } from '$app/navigation';
2
- import { asText } from '../services/command-palette-manager.svelte.js';
2
+ import { asText } from '../utilities/common.js';
3
3
  import { mdiOpenInNew } from '@mdi/js';
4
4
  export const Constants = {
5
5
  Socials: {
@@ -29,6 +29,7 @@ export const Constants = {
29
29
  Get: 'https://get.immich.app/',
30
30
  My: 'https://my.immich.app/',
31
31
  Store: 'https://immich.store/',
32
+ Awesome: 'https://awesome.immich.app/',
32
33
  Ui: 'https://ui.immich.app/',
33
34
  },
34
35
  Pages: {
package/dist/types.d.ts CHANGED
@@ -28,6 +28,7 @@ export type IconLike = string | {
28
28
  path: string;
29
29
  };
30
30
  export type MaybeArray<T> = T | T[];
31
+ export type MaybePromise<T> = T | Promise<T>;
31
32
  export type IconProps = {
32
33
  icon: IconLike;
33
34
  title?: string;
@@ -31,3 +31,4 @@ export declare const resolveMetadata: (site: Metadata, page?: Metadata, article?
31
31
  tags: string[] | undefined;
32
32
  } | undefined;
33
33
  };
34
+ export declare const asText: (...items: unknown[]) => string;
@@ -60,3 +60,10 @@ export const resolveMetadata = (site, page, article) => {
60
60
  : undefined,
61
61
  };
62
62
  };
63
+ export const asText = (...items) => {
64
+ return items
65
+ .filter((item) => item !== undefined && item !== null)
66
+ .map((items) => String(items))
67
+ .join('|')
68
+ .toLowerCase();
69
+ };
@@ -1,4 +1,4 @@
1
- import type { Color, IconLike, IfLike, TextColor } from '../types.js';
1
+ import type { ActionItem, Color, IconLike, IfLike, 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;
@@ -10,3 +10,5 @@ export declare const resolveIcon: ({ icons, color, override, fallback, }: {
10
10
  icons: Partial<Record<Color | TextColor, string>>;
11
11
  }) => IconLike | undefined;
12
12
  export declare const isEnabled: ({ $if }: IfLike) => boolean;
13
+ export declare const asArray: <T>(items?: MaybeArray<T>) => T[];
14
+ export declare const getSearchString: ({ title, description, type, searchText }: ActionItem) => string;
@@ -1,3 +1,4 @@
1
+ import { asText } from './common.js';
1
2
  import { twMerge } from 'tailwind-merge';
2
3
  export const cleanClass = (...classNames) => {
3
4
  return twMerge(classNames
@@ -25,3 +26,5 @@ export const resolveIcon = ({ icons, color, override, fallback, }) => {
25
26
  return icons[color] ?? fallback;
26
27
  };
27
28
  export const isEnabled = ({ $if }) => $if?.() ?? true;
29
+ export const asArray = (items) => (Array.isArray(items) ? items : items ? [items] : []);
30
+ 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.51.0",
3
+ "version": "0.52.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,18 +0,0 @@
1
- <script lang="ts">
2
- import { commandPaletteManager } from '../../services/command-palette-manager.svelte';
3
- import type { ActionItem } from '../../types.js';
4
- import { untrack } from 'svelte';
5
-
6
- type Props = {
7
- commands?: ActionItem[];
8
- };
9
-
10
- const { commands = [] }: Props = $props();
11
-
12
- $effect(() => {
13
- // prevent reactivity loop
14
- const addCommands = (commands: ActionItem[]) => untrack(() => commandPaletteManager.addCommands(commands));
15
-
16
- return addCommands(commands);
17
- });
18
- </script>
@@ -1,7 +0,0 @@
1
- import type { ActionItem } from '../../types.js';
2
- type Props = {
3
- commands?: ActionItem[];
4
- };
5
- declare const CommandPaletteContext: import("svelte").Component<Props, {}, "">;
6
- type CommandPaletteContext = ReturnType<typeof CommandPaletteContext>;
7
- export default CommandPaletteContext;