@immich/ui 0.62.2 → 0.64.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/ContextMenu/ContextMenu.svelte +12 -12
- package/dist/components/FormModal/FormModal.svelte +3 -1
- package/dist/components/FormModal/FormModal.svelte.d.ts +1 -0
- package/dist/components/Modal/Modal.svelte +3 -0
- package/dist/components/Modal/Modal.svelte.d.ts +1 -0
- package/dist/components/Textarea/Textarea.svelte +4 -9
- package/dist/internal/Select.svelte +154 -59
- package/package.json +3 -3
|
@@ -22,14 +22,17 @@
|
|
|
22
22
|
}: ContextMenuProps = $props();
|
|
23
23
|
|
|
24
24
|
const itemStyles = tv({
|
|
25
|
-
base: '
|
|
25
|
+
base: 'data-highlighted:bg-light-200 flex items-center gap-1 rounded-lg p-1 text-start outline-none hover:cursor-pointer',
|
|
26
26
|
variants: {
|
|
27
27
|
color: styleVariants.textColor,
|
|
28
|
+
inset: {
|
|
29
|
+
true: 'mx-1',
|
|
30
|
+
},
|
|
28
31
|
},
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
const wrapperStyles = tv({
|
|
32
|
-
base: 'bg-light-100 dark:border-light-300 flex flex-col gap-1 overflow-hidden rounded-xl border py-1 shadow-sm',
|
|
35
|
+
base: 'bg-light-100 dark:border-light-300 flex flex-col gap-1 overflow-hidden rounded-xl border py-1 shadow-sm outline-none',
|
|
33
36
|
variants: {
|
|
34
37
|
size: {
|
|
35
38
|
tiny: 'w-32',
|
|
@@ -130,14 +133,12 @@
|
|
|
130
133
|
textValue={item.title}
|
|
131
134
|
closeOnSelect
|
|
132
135
|
onSelect={() => item.onAction(item)}
|
|
133
|
-
class=
|
|
136
|
+
class={itemStyles({ color: item.color, inset: true })}
|
|
134
137
|
>
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<Text class="grow text-start font-medium select-none" size="medium">{item.title}</Text>
|
|
140
|
-
</div>
|
|
138
|
+
{#if item.icon}
|
|
139
|
+
<Icon icon={item.icon} class="m-2 shrink-0" />
|
|
140
|
+
{/if}
|
|
141
|
+
<Text class="grow text-start font-medium select-none" size="medium">{item.title}</Text>
|
|
141
142
|
</DropdownMenu.Item>
|
|
142
143
|
{/if}
|
|
143
144
|
{/each}
|
|
@@ -152,10 +153,9 @@
|
|
|
152
153
|
closeOnSelect
|
|
153
154
|
onSelect={() => item.onAction(item)}
|
|
154
155
|
title={item.title}
|
|
156
|
+
class={itemStyles({ color: item.color })}
|
|
155
157
|
>
|
|
156
|
-
<
|
|
157
|
-
<Icon icon={item.icon} class="m-2 shrink-0" />
|
|
158
|
-
</div>
|
|
158
|
+
<Icon icon={item.icon} class="m-2 shrink-0" />
|
|
159
159
|
</DropdownMenu.Item>
|
|
160
160
|
{/if}
|
|
161
161
|
{/each}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
size?: ModalSize;
|
|
21
21
|
preventDefault?: boolean;
|
|
22
22
|
onClose: () => void;
|
|
23
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
23
24
|
onReset?: (event: Event) => void;
|
|
24
25
|
onSubmit: (event: SubmitEvent) => void;
|
|
25
26
|
children: Snippet<[{ formId: string }]>;
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
size = 'small',
|
|
37
38
|
preventDefault = true,
|
|
38
39
|
onClose = () => {},
|
|
40
|
+
onOpenAutoFocus,
|
|
39
41
|
onReset,
|
|
40
42
|
onSubmit,
|
|
41
43
|
children,
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
const formId = generateId();
|
|
61
63
|
</script>
|
|
62
64
|
|
|
63
|
-
<Modal {title} {onClose} {size} {icon}>
|
|
65
|
+
<Modal {title} {onClose} {size} {icon} {onOpenAutoFocus}>
|
|
64
66
|
<ModalBody>
|
|
65
67
|
<form {onsubmit} {onreset} id={formId}>
|
|
66
68
|
{@render children({ formId })}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
children: Snippet;
|
|
29
29
|
onClose?: () => void;
|
|
30
30
|
onEscapeKeydown?: (event: KeyboardEvent) => void;
|
|
31
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
let {
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
closeOnEsc = true,
|
|
41
42
|
closeOnBackdropClick = false,
|
|
42
43
|
children,
|
|
44
|
+
onOpenAutoFocus,
|
|
43
45
|
}: Props = $props();
|
|
44
46
|
|
|
45
47
|
const modalStyles = tv({
|
|
@@ -105,6 +107,7 @@
|
|
|
105
107
|
<Dialog.Portal>
|
|
106
108
|
<Dialog.Overlay class="{zIndex.ModalBackdrop} fixed start-0 top-0 flex h-dvh max-h-dvh w-screen bg-black/30" />
|
|
107
109
|
<Dialog.Content
|
|
110
|
+
{onOpenAutoFocus}
|
|
108
111
|
onEscapeKeydown={handleEscapeKeydown}
|
|
109
112
|
{escapeKeydownBehavior}
|
|
110
113
|
{interactOutsideBehavior}
|
|
@@ -11,6 +11,7 @@ type Props = {
|
|
|
11
11
|
children: Snippet;
|
|
12
12
|
onClose?: () => void;
|
|
13
13
|
onEscapeKeydown?: (event: KeyboardEvent) => void;
|
|
14
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
14
15
|
};
|
|
15
16
|
declare const Modal: import("svelte").Component<Props, {}, "">;
|
|
16
17
|
type Modal = ReturnType<typeof Modal>;
|
|
@@ -5,8 +5,6 @@
|
|
|
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';
|
|
9
|
-
import type { FormEventHandler } from 'svelte/elements';
|
|
10
8
|
import { tv } from 'tailwind-variants';
|
|
11
9
|
|
|
12
10
|
let {
|
|
@@ -75,12 +73,10 @@
|
|
|
75
73
|
}
|
|
76
74
|
};
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
onMount(() => autogrow(ref));
|
|
76
|
+
$effect(() => {
|
|
77
|
+
void value;
|
|
78
|
+
autogrow(ref);
|
|
79
|
+
});
|
|
84
80
|
</script>
|
|
85
81
|
|
|
86
82
|
<div class="flex w-full flex-col gap-1" bind:this={containerRef}>
|
|
@@ -94,7 +90,6 @@
|
|
|
94
90
|
|
|
95
91
|
<div class="relative w-full">
|
|
96
92
|
<textarea
|
|
97
|
-
oninput={onInput}
|
|
98
93
|
id={inputId}
|
|
99
94
|
aria-labelledby={label && labelId}
|
|
100
95
|
required={!!required}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends string">
|
|
2
2
|
import { getFieldContext } from '../common/context.svelte.js';
|
|
3
3
|
import Icon from '../components/Icon/Icon.svelte';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import Label from '../components/Label/Label.svelte';
|
|
5
|
+
import Text from '../components/Text/Text.svelte';
|
|
6
6
|
import { zIndex } from '../constants.js';
|
|
7
|
-
import {
|
|
7
|
+
import { styleVariants } from '../styles.js';
|
|
8
8
|
import type { SelectCommonProps, SelectOption } from '../types.js';
|
|
9
|
-
import { cleanClass } from '../utilities/internal.js';
|
|
9
|
+
import { cleanClass, generateId } from '../utilities/internal.js';
|
|
10
10
|
import { mdiArrowDown, mdiArrowUp, mdiCheck, mdiChevronDown } from '@mdi/js';
|
|
11
11
|
import { Select } from 'bits-ui';
|
|
12
12
|
import { tv } from 'tailwind-variants';
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
let {
|
|
23
23
|
options: optionsOrItems,
|
|
24
|
-
shape,
|
|
24
|
+
shape = 'semi-round',
|
|
25
25
|
size: initialSize,
|
|
26
26
|
multiple = false,
|
|
27
27
|
values = $bindable([]),
|
|
@@ -44,34 +44,105 @@
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
const context = getFieldContext();
|
|
47
|
-
const { invalid, disabled, ...labelProps } = $derived(context());
|
|
47
|
+
const { label, description, required, invalid, disabled, ...labelProps } = $derived(context());
|
|
48
48
|
const size = $derived(initialSize ?? labelProps.size ?? 'small');
|
|
49
|
+
const roundedSize = $derived(shape === 'semi-round' ? size : undefined);
|
|
49
50
|
const options = $derived(asOptions(optionsOrItems));
|
|
50
51
|
|
|
51
52
|
const findOption = (value: string) => options.find((option) => option.value === value);
|
|
52
53
|
const valuesToOptions = (values: T[]) =>
|
|
53
54
|
values.map(findOption).filter((item): item is SelectOption<T> => Boolean(item));
|
|
54
55
|
|
|
56
|
+
let open = $state(false);
|
|
57
|
+
let triggerRef = $state<HTMLButtonElement | null>(null);
|
|
58
|
+
let internalValue = $derived(multiple ? values : (values[0] ?? ''));
|
|
55
59
|
const selectedLabel = $derived(asLabel(valuesToOptions(values)));
|
|
56
60
|
|
|
61
|
+
const id = generateId();
|
|
62
|
+
const triggerId = `trigger-${id}`;
|
|
63
|
+
const descriptionId = $derived(description ? `description-${id}` : undefined);
|
|
64
|
+
|
|
57
65
|
const triggerStyles = tv({
|
|
58
|
-
base: 'w-full gap-1
|
|
66
|
+
base: 'w-full gap-1 p-0 text-start ring-1 ring-gray-200 outline-none focus-visible:outline-none dark:ring-neutral-900',
|
|
59
67
|
variants: {
|
|
68
|
+
disabled: {
|
|
69
|
+
true: 'cursor-not-allowed',
|
|
70
|
+
false: 'cursor-pointer',
|
|
71
|
+
},
|
|
72
|
+
shape: styleVariants.shape,
|
|
73
|
+
roundedSize: styleVariants.inputRoundedSize,
|
|
60
74
|
invalid: {
|
|
61
|
-
true: '
|
|
75
|
+
true: 'ring-danger-300 dark:ring-danger-300 focus-visible:ring-danger dark:focus-visible:ring-danger',
|
|
76
|
+
false: 'focus-visible:ring-primary dark:focus-visible:ring-primary',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const containerStyles = tv({
|
|
82
|
+
base: 'flex w-full items-center bg-gray-100 dark:bg-gray-800',
|
|
83
|
+
variants: {
|
|
84
|
+
shape: styleVariants.shape,
|
|
85
|
+
roundedSize: styleVariants.inputRoundedSize,
|
|
86
|
+
disabled: {
|
|
87
|
+
true: 'bg-light-300 dark:bg-gray-900',
|
|
62
88
|
false: '',
|
|
63
89
|
},
|
|
64
90
|
},
|
|
65
91
|
});
|
|
66
92
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
const valueStyles = tv({
|
|
94
|
+
base: 'block w-full min-w-0 flex-1 truncate py-2.5 text-start',
|
|
95
|
+
variants: {
|
|
96
|
+
textSize: styleVariants.textSize,
|
|
97
|
+
height: {
|
|
98
|
+
tiny: 'h-9',
|
|
99
|
+
small: 'h-10',
|
|
100
|
+
medium: 'h-11',
|
|
101
|
+
large: 'h-12',
|
|
102
|
+
giant: 'h-12',
|
|
103
|
+
},
|
|
104
|
+
leadingPadding: {
|
|
105
|
+
base: 'pl-4',
|
|
106
|
+
icon: 'pl-0',
|
|
107
|
+
},
|
|
108
|
+
trailingPadding: {
|
|
109
|
+
base: 'pr-4',
|
|
110
|
+
icon: 'pr-0',
|
|
111
|
+
},
|
|
112
|
+
roundedSize: styleVariants.inputRoundedSize,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
116
|
+
const contentStyles = tv({
|
|
117
|
+
base: cleanClass(
|
|
118
|
+
'text-dark dark:bg-primary-100 bg-light-100 max-h-96 w-(--bits-select-anchor-width) min-w-(--bits-select-anchor-width) border py-3 outline-none select-none',
|
|
119
|
+
zIndex.SelectDropdown,
|
|
120
|
+
),
|
|
121
|
+
variants: {
|
|
122
|
+
shape: {
|
|
123
|
+
rectangle: 'rounded-none',
|
|
124
|
+
'semi-round': '',
|
|
125
|
+
round: 'rounded-2xl',
|
|
126
|
+
},
|
|
127
|
+
roundedSize: styleVariants.inputRoundedSize,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const itemStyles = tv({
|
|
132
|
+
base: 'hover:bg-light-200 hover:dark:bg-primary-200 data-highlighted:bg-light-200 dark:data-highlighted:bg-primary-200 flex w-full items-center duration-75 outline-none select-none data-disabled:opacity-50',
|
|
133
|
+
variants: {
|
|
134
|
+
size: {
|
|
135
|
+
tiny: 'h-9 px-4 text-xs',
|
|
136
|
+
small: 'h-10 px-5 text-sm',
|
|
137
|
+
medium: 'h-11 px-5 text-base',
|
|
138
|
+
large: 'h-12 px-5 text-lg',
|
|
139
|
+
giant: 'h-12 px-6 text-xl',
|
|
140
|
+
},
|
|
141
|
+
disabled: {
|
|
142
|
+
true: 'cursor-not-allowed',
|
|
143
|
+
false: 'cursor-pointer',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
75
146
|
});
|
|
76
147
|
|
|
77
148
|
const onValueChange = (newValues: string[] | string) => {
|
|
@@ -83,61 +154,85 @@
|
|
|
83
154
|
onChange?.(values);
|
|
84
155
|
onItemChange?.(items);
|
|
85
156
|
};
|
|
86
|
-
|
|
87
|
-
let internalValue = $derived(multiple ? values : (values[0] ?? ''));
|
|
88
157
|
</script>
|
|
89
158
|
|
|
90
|
-
<div class={cleanClass('flex flex-col gap-1', className)}
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
159
|
+
<div class={cleanClass('flex w-full flex-col gap-1', className)}>
|
|
160
|
+
{#if label}
|
|
161
|
+
<Label
|
|
162
|
+
{...labelProps}
|
|
163
|
+
{label}
|
|
164
|
+
{size}
|
|
165
|
+
requiredIndicator={required === 'indicator'}
|
|
166
|
+
for={triggerId}
|
|
167
|
+
onclick={() => {
|
|
168
|
+
if (disabled) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
open = true;
|
|
172
|
+
triggerRef?.focus();
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
{/if}
|
|
176
|
+
|
|
177
|
+
{#if description}
|
|
178
|
+
<Text color="muted" size="small" id={descriptionId} class="mb-2">{description}</Text>
|
|
179
|
+
{/if}
|
|
180
|
+
|
|
181
|
+
<Select.Root
|
|
182
|
+
type={multiple ? 'multiple' : 'single'}
|
|
183
|
+
bind:value={internalValue as never}
|
|
184
|
+
bind:open
|
|
185
|
+
items={options.map(({ value, label, disabled }) => ({ value, label: label ?? value, disabled }))}
|
|
186
|
+
{onValueChange}
|
|
187
|
+
>
|
|
188
|
+
<Select.Trigger
|
|
189
|
+
bind:ref={triggerRef}
|
|
190
|
+
{disabled}
|
|
191
|
+
id={triggerId}
|
|
192
|
+
class={triggerStyles({
|
|
193
|
+
disabled,
|
|
194
|
+
invalid,
|
|
195
|
+
shape,
|
|
196
|
+
roundedSize,
|
|
197
|
+
})}
|
|
198
|
+
aria-describedby={descriptionId}
|
|
199
|
+
aria-label={label ? undefined : placeholder}
|
|
200
|
+
>
|
|
201
|
+
<div
|
|
202
|
+
class={containerStyles({
|
|
203
|
+
disabled,
|
|
204
|
+
shape,
|
|
205
|
+
roundedSize,
|
|
206
|
+
})}
|
|
101
207
|
>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
208
|
+
<span
|
|
209
|
+
class={cleanClass(
|
|
210
|
+
valueStyles({
|
|
211
|
+
textSize: size,
|
|
212
|
+
height: size,
|
|
213
|
+
leadingPadding: 'base',
|
|
214
|
+
trailingPadding: 'icon',
|
|
215
|
+
roundedSize,
|
|
216
|
+
}),
|
|
217
|
+
!selectedLabel && placeholder && 'text-gray-600 dark:text-gray-400',
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{selectedLabel || placeholder}
|
|
221
|
+
</span>
|
|
222
|
+
|
|
223
|
+
<Icon icon={mdiChevronDown} class={cleanClass('mx-3 shrink-0')} aria-hidden />
|
|
224
|
+
</div>
|
|
115
225
|
</Select.Trigger>
|
|
116
226
|
<Select.Portal>
|
|
117
|
-
<Select.Content
|
|
118
|
-
bind:ref={contentRef}
|
|
119
|
-
class="text-dark dark:bg-primary-100 bg-light-100 max-h-96 rounded-xl border py-3 outline-none select-none {zIndex.SelectDropdown}"
|
|
120
|
-
customAnchor={inputRef}
|
|
121
|
-
sideOffset={4}
|
|
122
|
-
>
|
|
227
|
+
<Select.Content class={contentStyles({ shape, roundedSize })} sideOffset={4}>
|
|
123
228
|
<Select.ScrollUpButton class="flex w-full items-center justify-center">
|
|
124
229
|
<Icon icon={mdiArrowUp} />
|
|
125
230
|
</Select.ScrollUpButton>
|
|
126
231
|
<Select.Viewport>
|
|
127
232
|
{#each options as { value, label, disabled }, i (i + value)}
|
|
128
|
-
<Select.Item
|
|
129
|
-
class={cleanClass(
|
|
130
|
-
'hover:bg-light-200 hover:dark:bg-primary-200 data-selected:bg-light-200 dark:data-selected:bg-primary-200 flex h-10 w-full items-center px-5 py-3 text-sm duration-75 outline-none select-none data-disabled:opacity-50',
|
|
131
|
-
disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
132
|
-
)}
|
|
133
|
-
{value}
|
|
134
|
-
{label}
|
|
135
|
-
{disabled}
|
|
136
|
-
>
|
|
233
|
+
<Select.Item class={cleanClass(itemStyles({ size, disabled }))} {value} {label} {disabled}>
|
|
137
234
|
{#snippet children({ selected })}
|
|
138
|
-
<div
|
|
139
|
-
class="flex items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-colors"
|
|
140
|
-
>
|
|
235
|
+
<div class="flex items-center justify-center gap-2 font-medium whitespace-nowrap transition-colors">
|
|
141
236
|
<span>{label}</span>
|
|
142
237
|
</div>
|
|
143
238
|
{#if selected}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@immich/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"license": "GNU Affero General Public License version 3",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@internationalized/date": "^3.10.0",
|
|
51
51
|
"@mdi/js": "^7.4.47",
|
|
52
|
-
"bits-ui": "^2.
|
|
52
|
+
"bits-ui": "^2.15.7",
|
|
53
53
|
"luxon": "^3.7.2",
|
|
54
54
|
"simple-icons": "^16.0.0",
|
|
55
55
|
"svelte-highlight": "^7.8.4",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@immich/svelte-markdown-preprocess": "^0.2.1"
|
|
59
59
|
},
|
|
60
60
|
"volta": {
|
|
61
|
-
"node": "24.13.
|
|
61
|
+
"node": "24.13.1"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"create": "node scripts/create.js",
|