@immich/ui 0.65.2 → 0.67.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.
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import { getFieldContext } from '../../common/context.svelte.js';
3
+ import Label from '../Label/Label.svelte';
4
+ import { styleVariants } from '../../styles.js';
5
+ import type { PinInputProps } from '../../types.js';
6
+ import { cleanClass } from '../../utilities/internal.js';
7
+ import { PinInput, REGEXP_ONLY_DIGITS } from 'bits-ui';
8
+ import { tv } from 'tailwind-variants';
9
+
10
+ let {
11
+ shape = 'semi-round',
12
+ size: initialSize,
13
+ value = $bindable<string>(''),
14
+ length = 6,
15
+ password,
16
+ onComplete,
17
+ class: className,
18
+ ...props
19
+ }: PinInputProps = $props();
20
+
21
+ const context = getFieldContext();
22
+
23
+ const { label, disabled, ...labelProps } = $derived(context());
24
+ const size = $derived(initialSize ?? labelProps.size ?? 'large');
25
+
26
+ const inputStyles = tv({
27
+ base: 'group-has-disabled:text-dark data-active:border-primary dark:data-active:border-primary flex items-center justify-center border-2 bg-gray-100 font-mono transition-all duration-75 group-has-disabled:bg-gray-300 data-active:border-3 dark:bg-gray-800 dark:group-not-has-disabled:border-gray-700 dark:group-has-disabled:bg-gray-900 dark:group-has-disabled:text-gray-200',
28
+ variants: {
29
+ shape: styleVariants.shape,
30
+ size: {
31
+ tiny: 'h-9 w-7',
32
+ small: 'h-10 w-8',
33
+ medium: 'h-11 w-9',
34
+ large: 'h-12 w-10',
35
+ giant: 'h-14 w-12',
36
+ },
37
+ textSize: styleVariants.textSize,
38
+ roundedSize: {
39
+ tiny: 'rounded-lg',
40
+ small: 'rounded-lg',
41
+ medium: 'rounded-xl',
42
+ large: 'rounded-xl',
43
+ giant: 'rounded-2xl',
44
+ },
45
+ },
46
+ });
47
+
48
+ const caretStyles = tv({
49
+ base: 'caret bg-dark h-1/2',
50
+ variants: {
51
+ size: {
52
+ tiny: 'w-px',
53
+ small: 'w-px',
54
+ medium: 'w-[1.5px]',
55
+ large: 'w-[1.5px]',
56
+ giant: 'w-0.5',
57
+ },
58
+ },
59
+ });
60
+
61
+ const id = $props.id();
62
+ const inputId = `input-${id}`;
63
+ const labelId = `label-${id}`;
64
+ </script>
65
+
66
+ <div class={cleanClass('flex flex-col gap-1', className)}>
67
+ {#if label}
68
+ <Label id={labelId} for={inputId} {label} {...labelProps} {size} />
69
+ {/if}
70
+
71
+ <PinInput.Root
72
+ {inputId}
73
+ aria-labelledby={label && labelId}
74
+ {disabled}
75
+ aria-disabled={disabled}
76
+ class="group flex w-fit items-center gap-2"
77
+ maxlength={length}
78
+ pattern={REGEXP_ONLY_DIGITS}
79
+ type={password ? 'password' : 'text'}
80
+ {onComplete}
81
+ bind:value
82
+ {...props}
83
+ >
84
+ {#snippet children({ cells })}
85
+ {#each cells as cell, i (i)}
86
+ <PinInput.Cell
87
+ {cell}
88
+ class={inputStyles({
89
+ shape,
90
+ size,
91
+ textSize: size,
92
+ roundedSize: shape === 'semi-round' ? size : undefined,
93
+ })}
94
+ >
95
+ {#if cell.char !== null}
96
+ <div>
97
+ {password ? '●' : cell.char}
98
+ </div>
99
+ {/if}
100
+ {#if cell.hasFakeCaret}
101
+ <div class="absolute flex h-full items-center justify-center">
102
+ <div class={caretStyles({ size })}></div>
103
+ </div>
104
+ {/if}
105
+ </PinInput.Cell>
106
+ {/each}
107
+ {/snippet}
108
+ </PinInput.Root>
109
+ </div>
110
+
111
+ <style>
112
+ .caret {
113
+ animation: blink 1.5s step-end infinite;
114
+ }
115
+ @keyframes blink {
116
+ 0%,
117
+ 100% {
118
+ opacity: 1;
119
+ }
120
+ 50% {
121
+ opacity: 0;
122
+ }
123
+ }
124
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { PinInputProps } from '../../types.js';
2
+ import { PinInput } from 'bits-ui';
3
+ declare const PinInput: import("svelte").Component<PinInputProps, {}, "value">;
4
+ type PinInput = ReturnType<typeof PinInput>;
5
+ export default PinInput;
@@ -12,8 +12,10 @@
12
12
  containerRef = $bindable(null),
13
13
  shape = 'semi-round',
14
14
  size: initialSize,
15
+ variant = 'input',
15
16
  class: className,
16
- grow,
17
+ rows = variant === 'ghost' ? 1 : undefined,
18
+ grow = variant === 'ghost',
17
19
  value = $bindable<string>(),
18
20
  ...restProps
19
21
  }: TextareaProps = $props();
@@ -22,18 +24,35 @@
22
24
  const { label, description, readOnly, required, invalid, disabled, ...labelProps } = $derived(context());
23
25
  const size = $derived(initialSize ?? labelProps.size ?? 'small');
24
26
 
25
- const styles = tv({
26
- base: 'focus-within:ring-primary dark:focus-within:ring-primary w-full bg-gray-100 ring-1 ring-gray-200 outline-none focus-within:ring-1 disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-800 dark:ring-black dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
27
+ const commonStyles = tv({
28
+ base: 'w-full outline-none disabled:cursor-not-allowed',
29
+ variants: {
30
+ textSize: styleVariants.textSize,
31
+ grow: { true: 'resize-none', false: '' },
32
+ },
33
+ });
34
+
35
+ const ghostStyles = tv({
36
+ base: 'pb-2',
37
+ variants: {
38
+ state: {
39
+ active:
40
+ 'hover:border-b-light-200 focus-within:hover:border-primary focus-within:border-primary focus-within:border-b-2 hover:border-b-2',
41
+ error: 'border-b-danger border-b-2',
42
+ disabled: '',
43
+ },
44
+ },
45
+ });
46
+
47
+ const inputStyles = tv({
48
+ base: 'focus-within:ring-primary dark:focus-within:ring-primary bg-gray-100 ring-1 ring-gray-200 focus-within:ring-1 disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-800 dark:ring-black dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
27
49
  variants: {
28
50
  shape: styleVariants.shape,
29
51
  padding: {
30
52
  base: 'px-4 py-3',
31
53
  round: 'px-5 py-3',
32
54
  },
33
- grow: {
34
- true: 'resize-none',
35
- false: '',
36
- },
55
+ invalid: { true: 'border-danger/80 border', false: '' },
37
56
  roundedSize: {
38
57
  tiny: 'rounded-xl',
39
58
  small: 'rounded-xl',
@@ -41,11 +60,6 @@
41
60
  large: 'rounded-2xl',
42
61
  giant: 'rounded-2xl',
43
62
  },
44
- textSize: styleVariants.textSize,
45
- invalid: {
46
- true: 'border-danger/80 border',
47
- false: '',
48
- },
49
63
  },
50
64
  });
51
65
 
@@ -54,21 +68,38 @@
54
68
  const labelId = `label-${id}`;
55
69
  const descriptionId = $derived(description ? `description-${id}` : undefined);
56
70
 
71
+ const parseStyleNumber = (raw: string) => {
72
+ if (raw === 'none') {
73
+ return;
74
+ }
75
+
76
+ const value = Number.parseFloat(raw);
77
+ if (Number.isNaN(value)) {
78
+ return;
79
+ }
80
+
81
+ return value;
82
+ };
83
+
57
84
  const autogrow = (element: HTMLTextAreaElement | null) => {
58
85
  if (element && grow) {
59
86
  element.style.minHeight = '0';
60
87
  element.style.height = 'auto';
61
- element.style.height = `${element.scrollHeight}px`;
88
+
89
+ const style = getComputedStyle(element);
90
+ const borderTopWidth = parseStyleNumber(style.borderTopWidth) ?? 0;
91
+ const borderBottomWidth = parseStyleNumber(style.borderBottomWidth) ?? 0;
92
+ const height = element.scrollHeight + borderTopWidth + borderBottomWidth;
93
+
94
+ element.style.height = `${height}px`;
62
95
 
63
96
  // Show scrollbar only if there is a max-height and content exceeds it
64
- const maxHeight = Number.parseFloat(getComputedStyle(element).maxHeight);
97
+ const maxHeight = parseStyleNumber(style.maxHeight);
65
98
  const hasMaxHeight = maxHeight !== undefined;
66
- if (hasMaxHeight && element.scrollHeight > maxHeight) {
99
+ if (hasMaxHeight && height > maxHeight) {
67
100
  element.style.overflow = 'auto';
68
- } else if (hasMaxHeight && element.scrollHeight <= maxHeight) {
69
- element.style.overflow = 'hidden';
70
101
  } else {
71
- element.style.overflow = '';
102
+ element.style.overflow = 'hidden';
72
103
  }
73
104
  }
74
105
  };
@@ -99,15 +130,17 @@
99
130
  aria-describedby={descriptionId}
100
131
  readonly={readOnly}
101
132
  aria-readonly={readOnly}
133
+ {rows}
102
134
  class={cleanClass(
103
- styles({
104
- shape,
105
- textSize: size,
106
- padding: shape === 'round' ? 'round' : 'base',
107
- grow,
108
- roundedSize: shape === 'semi-round' ? size : undefined,
109
- invalid,
110
- }),
135
+ commonStyles({ textSize: size, grow }),
136
+ variant === 'input' &&
137
+ inputStyles({
138
+ shape,
139
+ invalid,
140
+ padding: shape === 'round' ? 'round' : 'base',
141
+ roundedSize: shape === 'semi-round' ? size : undefined,
142
+ }),
143
+ variant === 'ghost' && ghostStyles({ state: invalid ? 'error' : disabled || readOnly ? 'disabled' : 'active' }),
111
144
  className,
112
145
  )}
113
146
  bind:this={ref}
@@ -1,15 +1,25 @@
1
1
  <script lang="ts">
2
+ import Button from '../Button/Button.svelte';
2
3
  import ToastContainer from './ToastContainer.svelte';
3
4
  import ToastContent from './ToastContent.svelte';
4
5
  import type { ToastProps } from '../../types.js';
5
6
 
6
- let { children, title, description, icon, onClose, ...props }: ToastProps = $props();
7
+ let { children, title, description, icon, onClose, button, ...props }: ToastProps = $props();
7
8
  </script>
8
9
 
9
10
  <ToastContainer {...props}>
10
11
  {#if children}
11
12
  {@render children()}
12
13
  {:else if title}
13
- <ToastContent {title} {description} {icon} {onClose} {...props} />
14
+ <ToastContent {title} {description} {icon} {onClose} {...props}>
15
+ {#if button}
16
+ {@const { label, ...rest } = button}
17
+ <div class="flex justify-end px-3 pt-2">
18
+ <Button color="secondary" size="small" {...rest}>
19
+ {label}
20
+ </Button>
21
+ </div>
22
+ {/if}
23
+ </ToastContent>
14
24
  {/if}
15
25
  </ToastContainer>
@@ -15,7 +15,7 @@
15
15
  }: ToastContainerProps = $props();
16
16
 
17
17
  const containerStyles = tv({
18
- base: 'bg-light text-dark overflow-hidden border py-1.5 shadow-xs transition-all',
18
+ base: 'bg-light text-dark overflow-hidden border py-3 shadow-xs transition-all',
19
19
  variants: {
20
20
  color: {
21
21
  primary: 'border-primary-100 bg-primary-50 dark:bg-primary-100 dark:border-primary-200',
@@ -58,7 +58,7 @@
58
58
  {/if}
59
59
  </div>
60
60
  <div class="ms-1 flex grow justify-between">
61
- <div class="flex flex-col p-2">
61
+ <div class="flex flex-col px-2">
62
62
  {#if title}
63
63
  <Text fontWeight="semi-bold" class={titleStyles({ color })}>{@render resolve(title)}</Text>
64
64
  {/if}
package/dist/index.d.ts CHANGED
@@ -70,6 +70,7 @@ export { default as NavbarGroup } from './components/Navbar/NavbarGroup.svelte';
70
70
  export { default as NavbarItem } from './components/Navbar/NavbarItem.svelte';
71
71
  export { default as NumberInput } from './components/NumberInput/NumberInput.svelte';
72
72
  export { default as PasswordInput } from './components/PasswordInput/PasswordInput.svelte';
73
+ export { default as PinInput } from './components/PinInput/PinInput.svelte';
73
74
  export { default as ProgressBar } from './components/ProgressBar/ProgressBar.svelte';
74
75
  export { default as Scrollable } from './components/Scrollable/Scrollable.svelte';
75
76
  export { default as Select } from './components/Select/Select.svelte';
@@ -103,9 +104,9 @@ export * from './services/theme.svelte.js';
103
104
  export * from './services/toast-manager.svelte.js';
104
105
  export * from './services/translation.svelte.js';
105
106
  export * from './state/locale-state.svelte.js';
107
+ export { isModalOpen } from './state/modal-state.svelte.js';
106
108
  export * from './types.js';
107
109
  export * from './utilities/byte-units.js';
108
110
  export * from './utilities/common.js';
109
- export { isModalOpen } from './state/modal-state.svelte.js';
110
111
  export * from './site/constants.js';
111
112
  export { default as SiteFooter } from './site/SiteFooter.svelte';
package/dist/index.js CHANGED
@@ -72,6 +72,7 @@ export { default as NavbarGroup } from './components/Navbar/NavbarGroup.svelte';
72
72
  export { default as NavbarItem } from './components/Navbar/NavbarItem.svelte';
73
73
  export { default as NumberInput } from './components/NumberInput/NumberInput.svelte';
74
74
  export { default as PasswordInput } from './components/PasswordInput/PasswordInput.svelte';
75
+ export { default as PinInput } from './components/PinInput/PinInput.svelte';
75
76
  export { default as ProgressBar } from './components/ProgressBar/ProgressBar.svelte';
76
77
  export { default as Scrollable } from './components/Scrollable/Scrollable.svelte';
77
78
  export { default as Select } from './components/Select/Select.svelte';
@@ -106,10 +107,10 @@ export * from './services/theme.svelte.js';
106
107
  export * from './services/toast-manager.svelte.js';
107
108
  export * from './services/translation.svelte.js';
108
109
  export * from './state/locale-state.svelte.js';
110
+ export { isModalOpen } from './state/modal-state.svelte.js';
109
111
  export * from './types.js';
110
112
  export * from './utilities/byte-units.js';
111
113
  export * from './utilities/common.js';
112
- export { isModalOpen } from './state/modal-state.svelte.js';
113
114
  // site
114
115
  export * from './site/constants.js';
115
116
  export { default as SiteFooter } from './site/SiteFooter.svelte';
package/dist/types.d.ts CHANGED
@@ -156,9 +156,21 @@ export type PasswordInputProps = BaseInputProps<string> & {
156
156
  translations?: TranslationProps<'show_password' | 'hide_password'>;
157
157
  isVisible?: boolean;
158
158
  };
159
+ export type PinInputProps = {
160
+ ref?: HTMLInputElement | null;
161
+ class?: string;
162
+ size?: Size;
163
+ value?: string;
164
+ shape?: Shape;
165
+ disabled?: boolean;
166
+ length?: number;
167
+ password?: boolean;
168
+ onComplete?: (value: string) => void;
169
+ };
159
170
  export type TextareaProps = {
160
171
  ref?: HTMLTextAreaElement | null;
161
172
  containerRef?: HTMLElement | null;
173
+ variant?: 'input' | 'ghost';
162
174
  class?: string;
163
175
  value?: string;
164
176
  size?: Size;
@@ -187,7 +199,7 @@ export type MultiSelectProps<T extends string> = SelectCommonProps<T> & {
187
199
  onChange?: (values: T[]) => void;
188
200
  onSelect?: (options: SelectOption<T>[]) => void;
189
201
  };
190
- export type ToastId = {
202
+ export type ToastWithId = ToastItem & {
191
203
  id: string;
192
204
  };
193
205
  type ToastCommonProps = {
@@ -199,13 +211,14 @@ export type ToastContentProps = ToastCommonProps & {
199
211
  icon?: IconLike | false;
200
212
  onClose?: () => void;
201
213
  children?: Snippet;
214
+ button?: ToastButton;
202
215
  };
203
216
  export type ToastContainerProps = ToastCommonProps & {
204
217
  shape?: Shape;
205
218
  size?: ContainerSize;
206
219
  } & Omit<HTMLAttributes<HTMLElement>, 'title' | 'color' | 'size'>;
207
220
  export type ToastPanelProps = {
208
- items: Array<ToastItem & ToastId>;
221
+ items: Array<ToastWithId>;
209
222
  } & HTMLAttributes<HTMLDivElement>;
210
223
  export type ToastProps = ToastContentProps & ToastContainerProps;
211
224
  type Closable = {
@@ -222,6 +235,7 @@ export type ToastShow = {
222
235
  shape?: Shape;
223
236
  icon?: IconLike | false;
224
237
  size?: ContainerSize;
238
+ button?: ToastButton;
225
239
  };
226
240
  export type ToastOptions = {
227
241
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.65.2",
3
+ "version": "0.67.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "repository": {
6
6
  "type": "git",