@immich/ui 0.49.3 → 0.50.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.
- package/dist/components/AppShell/AppShellSidebar.svelte +1 -1
- package/dist/components/Breadcrumbs/Breadcrumbs.svelte +1 -1
- package/dist/components/Card/Card.svelte +8 -11
- package/dist/components/ContextMenu/ContextMenu.svelte +34 -12
- package/dist/components/ContextMenu/ContextMenuButton.svelte +12 -1
- package/dist/components/Toast/ToastPanel.svelte +4 -7
- package/dist/internal/Button.svelte +14 -17
- package/dist/services/command-palette-manager.svelte.d.ts +64 -4
- package/dist/services/translation.svelte.d.ts +1 -0
- package/dist/services/translation.svelte.js +2 -0
- package/dist/types.d.ts +6 -9
- package/dist/utilities/common.d.ts +2 -0
- package/dist/utilities/common.js +4 -0
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
<Scrollable
|
|
19
19
|
class={cleanClass(
|
|
20
20
|
'bg-light text-dark absolute shrink-0 border-e shadow-lg transition-all duration-200 md:relative',
|
|
21
|
-
open ? `${zIndex.AppShellSidebar} w-[min(100vw,16rem)]` : 'w-
|
|
21
|
+
open ? `${zIndex.AppShellSidebar} w-[min(100vw,16rem)]` : 'w-0 border-e-0',
|
|
22
22
|
className,
|
|
23
23
|
)}
|
|
24
24
|
>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</span>
|
|
20
20
|
{/snippet}
|
|
21
21
|
|
|
22
|
-
<nav class={cleanClass('flex items-center gap-1', className)} {...props}>
|
|
22
|
+
<nav class={cleanClass('flex flex-wrap items-center gap-1', className)} {...props}>
|
|
23
23
|
{#each items as item, index (index)}
|
|
24
24
|
{#if index > 0}
|
|
25
25
|
{#if typeof separator === 'object' && 'text' in separator}
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import { cubicOut } from 'svelte/easing';
|
|
10
10
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
11
11
|
import { slide } from 'svelte/transition';
|
|
12
|
-
import { twMerge } from 'tailwind-merge';
|
|
13
12
|
import { tv } from 'tailwind-variants';
|
|
14
13
|
|
|
15
14
|
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
@@ -87,14 +86,12 @@
|
|
|
87
86
|
const headerPadding = $derived(headerBorder || !expanded);
|
|
88
87
|
|
|
89
88
|
const headerContainerClasses = $derived(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
headerChild?.class,
|
|
97
|
-
),
|
|
89
|
+
cleanClass(
|
|
90
|
+
headerContainerStyles({
|
|
91
|
+
padding: headerPadding,
|
|
92
|
+
border: headerBorder,
|
|
93
|
+
}),
|
|
94
|
+
headerChild?.class,
|
|
98
95
|
),
|
|
99
96
|
);
|
|
100
97
|
</script>
|
|
@@ -137,14 +134,14 @@
|
|
|
137
134
|
{#if bodyChild && expanded}
|
|
138
135
|
<div
|
|
139
136
|
transition:slide={{ duration: expandable ? 200 : 0, easing: cubicOut }}
|
|
140
|
-
class={
|
|
137
|
+
class={cleanClass('immich-scrollbar h-full w-full overflow-auto p-4', bodyChild?.class)}
|
|
141
138
|
>
|
|
142
139
|
{@render bodyChild?.snippet()}
|
|
143
140
|
</div>
|
|
144
141
|
{/if}
|
|
145
142
|
|
|
146
143
|
{#if footerChild}
|
|
147
|
-
<div class={
|
|
144
|
+
<div class={cleanClass('flex items-center border-t p-4', footerChild.class)}>
|
|
148
145
|
{@render footerChild.snippet()}
|
|
149
146
|
</div>
|
|
150
147
|
{/if}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import Text from '../Text/Text.svelte';
|
|
4
4
|
import { zIndex } from '../../constants.js';
|
|
5
5
|
import { styleVariants } from '../../styles.js';
|
|
6
|
-
import {
|
|
6
|
+
import { type ActionItem, type ContextMenuProps, type MenuItems } from '../../types.js';
|
|
7
|
+
import { isMenuItemType } from '../../utilities/common.js';
|
|
7
8
|
import { cleanClass, isEnabled } from '../../utilities/internal.js';
|
|
8
9
|
import { DropdownMenu } from 'bits-ui';
|
|
9
10
|
import { fly } from 'svelte/transition';
|
|
@@ -20,10 +21,6 @@
|
|
|
20
21
|
...restProps
|
|
21
22
|
}: ContextMenuProps = $props();
|
|
22
23
|
|
|
23
|
-
const isDivider = (item: ActionItem | MenuItemType): item is MenuItemType => {
|
|
24
|
-
return item === MenuItemType.Divider;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
24
|
const itemStyles = tv({
|
|
28
25
|
base: 'hover:bg-light-200 flex w-full items-center gap-1 rounded-lg p-1 text-start hover:cursor-pointer',
|
|
29
26
|
variants: {
|
|
@@ -83,10 +80,35 @@
|
|
|
83
80
|
}
|
|
84
81
|
};
|
|
85
82
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
const getFilteredItems = (items?: MenuItems) => {
|
|
84
|
+
if (!items) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const results = [];
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
if (item && (isMenuItemType(item) || isEnabled(item))) {
|
|
91
|
+
results.push(item);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// remove trailing dividers
|
|
97
|
+
for (let i = results.length - 1; i >= 0; i--) {
|
|
98
|
+
const item = results[i];
|
|
99
|
+
if (isMenuItemType(item)) {
|
|
100
|
+
results.pop();
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return results;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const filteredItems = $derived(getFilteredItems(items));
|
|
111
|
+
const filteredBottomItems = $derived(getFilteredItems(bottomItems) as ActionItem[]);
|
|
90
112
|
|
|
91
113
|
const alignOffset = $derived(target.clientWidth / 2);
|
|
92
114
|
const sideOffset = $derived(-target.clientHeight / 2);
|
|
@@ -100,8 +122,8 @@
|
|
|
100
122
|
{#if open}
|
|
101
123
|
<div {...wrapperProps} class={zIndex.ContextMenu}>
|
|
102
124
|
<div {...props} {...restProps} class={cleanClass(wrapperStyles({ size }), className)} transition:fly>
|
|
103
|
-
{#each filteredItems as item, i (
|
|
104
|
-
{#if
|
|
125
|
+
{#each filteredItems as item, i (isMenuItemType(item) ? i : item.title)}
|
|
126
|
+
{#if isMenuItemType(item)}
|
|
105
127
|
<DropdownMenu.Separator class="dark:border-light-300 my-0.5 border-t" />
|
|
106
128
|
{:else}
|
|
107
129
|
<DropdownMenu.Item
|
|
@@ -118,7 +140,7 @@
|
|
|
118
140
|
{/if}
|
|
119
141
|
{/each}
|
|
120
142
|
|
|
121
|
-
{#if filteredBottomItems}
|
|
143
|
+
{#if filteredBottomItems.length > 0}
|
|
122
144
|
<DropdownMenu.Separator class="dark:border-light-300 my-0.5 border-t" />
|
|
123
145
|
<div class="flex gap-1 px-1">
|
|
124
146
|
{#each filteredBottomItems as item (item.title)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import IconButton from '../IconButton/IconButton.svelte';
|
|
3
3
|
import { menuManager } from '../../services/menu-manager.svelte.js';
|
|
4
|
+
import { t } from '../../services/translation.svelte.js';
|
|
4
5
|
import type { ContextMenuButtonProps } from '../../types.js';
|
|
5
6
|
import { mdiDotsVertical } from '@mdi/js';
|
|
6
7
|
|
|
@@ -10,8 +11,10 @@
|
|
|
10
11
|
icon = mdiDotsVertical,
|
|
11
12
|
variant = 'ghost',
|
|
12
13
|
shape = 'round',
|
|
14
|
+
'aria-label': ariaLabel,
|
|
13
15
|
items,
|
|
14
16
|
bottomItems,
|
|
17
|
+
translations,
|
|
15
18
|
...rest
|
|
16
19
|
}: ContextMenuButtonProps = $props();
|
|
17
20
|
|
|
@@ -20,4 +23,12 @@
|
|
|
20
23
|
};
|
|
21
24
|
</script>
|
|
22
25
|
|
|
23
|
-
<IconButton
|
|
26
|
+
<IconButton
|
|
27
|
+
{icon}
|
|
28
|
+
{color}
|
|
29
|
+
{shape}
|
|
30
|
+
{variant}
|
|
31
|
+
aria-label={ariaLabel ?? t('open_menu', translations)}
|
|
32
|
+
{...rest}
|
|
33
|
+
{onclick}
|
|
34
|
+
/>
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { isCustomToast } from '../../services/toast-manager.svelte.js';
|
|
5
5
|
import type { ToastPanelProps } from '../../types.js';
|
|
6
6
|
import { cleanClass } from '../../utilities/internal.js';
|
|
7
|
-
import { twMerge } from 'tailwind-merge';
|
|
8
7
|
|
|
9
8
|
const { items, class: className, ...props }: ToastPanelProps = $props();
|
|
10
9
|
|
|
@@ -12,12 +11,10 @@
|
|
|
12
11
|
</script>
|
|
13
12
|
|
|
14
13
|
<div
|
|
15
|
-
class={
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
className,
|
|
20
|
-
),
|
|
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,
|
|
21
18
|
)}
|
|
22
19
|
{...props}
|
|
23
20
|
>
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
import { cleanClass } from '../utilities/internal.js';
|
|
8
8
|
import { Button as ButtonPrimitive } from 'bits-ui';
|
|
9
9
|
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
10
|
-
import { twMerge } from 'tailwind-merge';
|
|
11
10
|
import { tv } from 'tailwind-variants';
|
|
12
11
|
|
|
13
12
|
type InternalButtonProps = ButtonProps & {
|
|
@@ -99,22 +98,20 @@
|
|
|
99
98
|
|
|
100
99
|
const classList = $derived(
|
|
101
100
|
cleanClass(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
className,
|
|
117
|
-
),
|
|
101
|
+
buttonVariants({
|
|
102
|
+
shape,
|
|
103
|
+
fullWidth,
|
|
104
|
+
textPadding: icon ? undefined : size,
|
|
105
|
+
textSize: size,
|
|
106
|
+
iconSize: icon ? size : undefined,
|
|
107
|
+
disabled,
|
|
108
|
+
roundedSize: shape === 'semi-round' ? size : undefined,
|
|
109
|
+
filledColor: variant === 'filled' ? color : undefined,
|
|
110
|
+
filledColorHover: variant === 'filled' ? color : undefined,
|
|
111
|
+
outlineColor: variant === 'outline' ? color : undefined,
|
|
112
|
+
ghostColor: variant === 'ghost' ? color : undefined,
|
|
113
|
+
}),
|
|
114
|
+
className,
|
|
118
115
|
),
|
|
119
116
|
);
|
|
120
117
|
|
|
@@ -4,16 +4,76 @@ export declare const asText: (...items: unknown[]) => string;
|
|
|
4
4
|
declare class CommandPaletteManager {
|
|
5
5
|
#private;
|
|
6
6
|
selectedIndex: number;
|
|
7
|
-
items: (
|
|
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 & {
|
|
8
23
|
id: string;
|
|
9
24
|
})[];
|
|
10
|
-
filteredItems: (
|
|
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 & {
|
|
11
41
|
id: string;
|
|
12
42
|
})[];
|
|
13
|
-
recentItems: (
|
|
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 & {
|
|
14
59
|
id: string;
|
|
15
60
|
})[];
|
|
16
|
-
results: (
|
|
61
|
+
results: ({
|
|
62
|
+
title: string;
|
|
63
|
+
description?: string;
|
|
64
|
+
type?: string;
|
|
65
|
+
searchText?: string;
|
|
66
|
+
icon: import("../types.js").IconLike;
|
|
67
|
+
iconClass?: string;
|
|
68
|
+
color?: import("../types.js").Color;
|
|
69
|
+
onAction: import("../types.js").ActionItemHandler;
|
|
70
|
+
shortcuts?: MaybeArray<import("../actions/shortcut.js").Shortcut>;
|
|
71
|
+
shortcutOptions?: {
|
|
72
|
+
ignoreInputFields?: boolean;
|
|
73
|
+
preventDefault?: boolean;
|
|
74
|
+
};
|
|
75
|
+
isGlobal?: boolean;
|
|
76
|
+
} & import("../types.js").IfLike & {
|
|
17
77
|
id: string;
|
|
18
78
|
})[];
|
|
19
79
|
get isEnabled(): boolean;
|
|
@@ -17,6 +17,8 @@ const defaultTranslations = {
|
|
|
17
17
|
hide_password: 'Hide password',
|
|
18
18
|
// theme switcher
|
|
19
19
|
dark_theme: 'Toggle dark theme',
|
|
20
|
+
// context menu
|
|
21
|
+
open_menu: 'Open menu',
|
|
20
22
|
// command palette
|
|
21
23
|
command_palette_prompt_default: 'Quickly find pages, actions, or commands',
|
|
22
24
|
command_palette_to_select: 'to select',
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import type { Shortcut } from './actions/shortcut.js';
|
|
1
2
|
import type { Translations } from './services/translation.svelte.js';
|
|
2
3
|
import type { DateTime } from 'luxon';
|
|
3
4
|
import type { Component, Snippet } from 'svelte';
|
|
4
5
|
import type { HTMLAnchorAttributes, HTMLAttributes, HTMLButtonAttributes, HTMLInputAttributes, HTMLLabelAttributes, HTMLTextareaAttributes } from 'svelte/elements';
|
|
5
|
-
import type { Shortcut } from './actions/shortcut.js';
|
|
6
6
|
export type Color = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
|
|
7
7
|
export type TextColor = Color | 'muted';
|
|
8
8
|
export type TextVariant = 'italic';
|
|
@@ -71,9 +71,9 @@ export type CloseButtonProps = {
|
|
|
71
71
|
export type ContextMenuButtonProps = ButtonBase & {
|
|
72
72
|
icon?: IconLike;
|
|
73
73
|
position?: ContextMenuPosition;
|
|
74
|
-
'aria-label': string;
|
|
75
74
|
items: MenuItems;
|
|
76
75
|
bottomItems?: Array<ActionItem | undefined>;
|
|
76
|
+
translations?: TranslationProps<'open_menu'>;
|
|
77
77
|
} & Omit<HTMLButtonAttributes, 'color' | 'size'>;
|
|
78
78
|
export type IconButtonProps = ButtonBase & {
|
|
79
79
|
icon: IconLike;
|
|
@@ -243,8 +243,8 @@ export type DatePickerProps = {
|
|
|
243
243
|
export type IfLike = {
|
|
244
244
|
$if?: () => boolean;
|
|
245
245
|
};
|
|
246
|
-
export type ActionItemHandler<T =
|
|
247
|
-
export type ActionItem
|
|
246
|
+
export type ActionItemHandler<T extends ActionItem = ActionItem> = (item: T) => void | Promise<void>;
|
|
247
|
+
export type ActionItem = {
|
|
248
248
|
title: string;
|
|
249
249
|
description?: string;
|
|
250
250
|
type?: string;
|
|
@@ -252,17 +252,14 @@ export type ActionItem<T = never> = Omit<{
|
|
|
252
252
|
icon: IconLike;
|
|
253
253
|
iconClass?: string;
|
|
254
254
|
color?: Color;
|
|
255
|
-
onAction: ActionItemHandler
|
|
256
|
-
data: T;
|
|
255
|
+
onAction: ActionItemHandler;
|
|
257
256
|
shortcuts?: MaybeArray<Shortcut>;
|
|
258
257
|
shortcutOptions?: {
|
|
259
258
|
ignoreInputFields?: boolean;
|
|
260
259
|
preventDefault?: boolean;
|
|
261
260
|
};
|
|
262
261
|
isGlobal?: boolean;
|
|
263
|
-
} & IfLike
|
|
264
|
-
T
|
|
265
|
-
] extends [never] ? 'data' : ''>;
|
|
262
|
+
} & IfLike;
|
|
266
263
|
export type BreadcrumbsProps = {
|
|
267
264
|
separator?: IconLike | {
|
|
268
265
|
text: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MenuItemType, type ActionItem } from '../types.js';
|
|
1
2
|
import type { DateTime } from 'luxon';
|
|
2
3
|
export declare const resolveUrl: (url: string, currentHostname?: string) => string;
|
|
3
4
|
export declare const isExternalLink: (href: string) => boolean;
|
|
@@ -14,6 +15,7 @@ export type ArticleMetadata = {
|
|
|
14
15
|
section?: string;
|
|
15
16
|
tags?: string[];
|
|
16
17
|
};
|
|
18
|
+
export declare const isMenuItemType: (item: ActionItem | MenuItemType) => item is MenuItemType;
|
|
17
19
|
export declare const resolveMetadata: (site: Metadata, page?: Metadata, article?: ArticleMetadata) => {
|
|
18
20
|
type: string;
|
|
19
21
|
siteName: string;
|
package/dist/utilities/common.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { env } from '$env/dynamic/public';
|
|
2
|
+
import { MenuItemType } from '../types.js';
|
|
2
3
|
const getImmichApp = (host) => {
|
|
3
4
|
if (!host || !host.endsWith('immich.app')) {
|
|
4
5
|
return false;
|
|
@@ -25,6 +26,9 @@ export const resolveUrl = (url, currentHostname) => {
|
|
|
25
26
|
export const isExternalLink = (href) => {
|
|
26
27
|
return !(href.startsWith('/') || href.startsWith('#'));
|
|
27
28
|
};
|
|
29
|
+
export const isMenuItemType = (item) => {
|
|
30
|
+
return item === MenuItemType.Divider;
|
|
31
|
+
};
|
|
28
32
|
export const resolveMetadata = (site, page, article) => {
|
|
29
33
|
const title = page ? `${page.title} | ${site.title}` : site.title;
|
|
30
34
|
const description = page?.description ?? site.description;
|