@immich/ui 0.28.1 → 0.30.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.
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { getFieldContext } from '../../common/context.svelte.js';
3
+ import Icon from '../Icon/Icon.svelte';
3
4
  import Label from '../Label/Label.svelte';
4
5
  import Text from '../Text/Text.svelte';
5
6
  import type { InputProps } from '../../types.js';
6
7
  import { cleanClass, generateId, isIconLike } from '../../utils.js';
7
- import Icon from '../Icon/Icon.svelte';
8
8
  import { tv } from 'tailwind-variants';
9
9
 
10
10
  let {
@@ -16,6 +16,8 @@
16
16
  value = $bindable<string>(),
17
17
  leadingIcon,
18
18
  trailingIcon,
19
+ trailingText,
20
+ inputSize,
19
21
  ...restProps
20
22
  }: InputProps = $props();
21
23
 
@@ -23,7 +25,7 @@
23
25
  $derived(getFieldContext());
24
26
 
25
27
  const iconStyles = tv({
26
- base: 'absolute inset-y-0 flex items-center justify-around',
28
+ base: 'flex flex-shrink-0 items-center justify-center',
27
29
  variants: {
28
30
  size: {
29
31
  tiny: 'w-6',
@@ -35,18 +37,14 @@
35
37
  },
36
38
  });
37
39
 
38
- const inputStyles = tv({
39
- base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
40
+ const containerStyles = tv({
41
+ base: 'flex w-full items-center bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
40
42
  variants: {
41
43
  shape: {
42
44
  rectangle: 'rounded-none',
43
45
  'semi-round': '',
44
46
  round: 'rounded-full',
45
47
  },
46
- padding: {
47
- base: 'px-4 py-3',
48
- round: 'px-5 py-3',
49
- },
50
48
  roundedSize: {
51
49
  tiny: 'rounded-xl',
52
50
  small: 'rounded-xl',
@@ -54,21 +52,16 @@
54
52
  large: 'rounded-2xl',
55
53
  giant: 'rounded-2xl',
56
54
  },
57
- // match with Button `iconSize` variants
58
- paddingLeft: {
59
- tiny: 'ps-6',
60
- small: 'ps-8',
61
- medium: 'ps-10',
62
- large: 'ps-12',
63
- giant: 'ps-14',
64
- },
65
- paddingRight: {
66
- tiny: 'pe-6',
67
- small: 'pe-8',
68
- medium: 'pe-10',
69
- large: 'pe-12',
70
- giant: 'pe-14',
55
+ invalid: {
56
+ true: 'border-danger/80 border',
57
+ false: '',
71
58
  },
59
+ },
60
+ });
61
+
62
+ const inputStyles = tv({
63
+ base: 'flex-1 bg-transparent py-3 outline-none disabled:cursor-not-allowed',
64
+ variants: {
72
65
  textSize: {
73
66
  tiny: 'text-xs',
74
67
  small: 'text-sm',
@@ -76,9 +69,22 @@
76
69
  large: 'text-lg',
77
70
  giant: 'text-xl',
78
71
  },
79
- invalid: {
80
- true: 'border-danger/80 border',
81
- false: '',
72
+ leadingPadding: {
73
+ base: 'pl-4',
74
+ icon: 'pl-0',
75
+ },
76
+ trailingPadding: {
77
+ base: 'pr-4',
78
+ icon: 'pr-0',
79
+ },
80
+ },
81
+ });
82
+
83
+ const trailingTextStyles = tv({
84
+ variants: {
85
+ padding: {
86
+ base: 'px-4',
87
+ icon: 'pl-4',
82
88
  },
83
89
  },
84
90
  });
@@ -98,18 +104,26 @@
98
104
  <Text color="secondary" size="small" id={descriptionId}>{description}</Text>
99
105
  {/if}
100
106
 
101
- <div class="relative w-full">
107
+ <div
108
+ class={cleanClass(
109
+ containerStyles({
110
+ shape,
111
+ roundedSize: shape === 'semi-round' ? size : undefined,
112
+ invalid,
113
+ }),
114
+ className,
115
+ )}
116
+ >
102
117
  {#if leadingIcon}
103
118
  <div tabindex="-1" class={iconStyles({ size })}>
104
- {#if leadingIcon}
105
- {#if isIconLike(leadingIcon)}
106
- <Icon size="60%" icon={leadingIcon} />
107
- {:else}
108
- {@render leadingIcon()}
109
- {/if}
119
+ {#if isIconLike(leadingIcon)}
120
+ <Icon size="60%" icon={leadingIcon} />
121
+ {:else}
122
+ {@render leadingIcon()}
110
123
  {/if}
111
124
  </div>
112
125
  {/if}
126
+
113
127
  <input
114
128
  id={inputId}
115
129
  aria-labelledby={label && labelId}
@@ -119,31 +133,31 @@
119
133
  aria-disabled={disabled}
120
134
  aria-describedby={descriptionId}
121
135
  readonly={readOnly}
136
+ size={inputSize}
122
137
  aria-readonly={readOnly}
123
- class={cleanClass(
124
- inputStyles({
125
- shape,
126
- textSize: size,
127
- padding: shape === 'round' ? 'round' : 'base',
128
- paddingLeft: leadingIcon ? size : undefined,
129
- paddingRight: trailingIcon ? size : undefined,
130
- roundedSize: shape === 'semi-round' ? size : undefined,
131
- invalid,
132
- }),
133
- className,
134
- )}
138
+ class={inputStyles({
139
+ textSize: size,
140
+ leadingPadding: leadingIcon ? 'icon' : 'base',
141
+ trailingPadding: trailingIcon || trailingText ? 'icon' : 'base',
142
+ })}
135
143
  bind:this={ref}
136
144
  bind:value
137
145
  {...restProps}
138
146
  />
147
+ {#if trailingText}
148
+ <Text
149
+ {size}
150
+ color="muted"
151
+ class={trailingTextStyles({ padding: trailingIcon ? 'icon' : 'base' })}>{trailingText}</Text
152
+ >
153
+ {/if}
154
+
139
155
  {#if trailingIcon}
140
- <div tabindex="-1" class={cleanClass(iconStyles({ size }), 'end-0')}>
141
- {#if trailingIcon}
142
- {#if isIconLike(trailingIcon)}
143
- <Icon size="60%" icon={trailingIcon} />
144
- {:else}
145
- {@render trailingIcon()}
146
- {/if}
156
+ <div tabindex="-1" class={iconStyles({ size })}>
157
+ {#if isIconLike(trailingIcon)}
158
+ <Icon size="60%" icon={trailingIcon} />
159
+ {:else}
160
+ {@render trailingIcon()}
147
161
  {/if}
148
162
  </div>
149
163
  {/if}
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import { getFieldContext } from '../../common/context.svelte.js';
3
+ import Label from '../Label/Label.svelte';
4
+ import Text from '../Text/Text.svelte';
5
+ import type { TextareaProps } from '../../types.js';
6
+ import { cleanClass, generateId } from '../../utils.js';
7
+ import type { FormEventHandler } from 'svelte/elements';
8
+ import { tv } from 'tailwind-variants';
9
+
10
+ let {
11
+ ref = $bindable(null),
12
+ containerRef = $bindable(null),
13
+ shape = 'semi-round',
14
+ size = 'medium',
15
+ class: className,
16
+ grow,
17
+ value = $bindable<string>(),
18
+ ...restProps
19
+ }: TextareaProps = $props();
20
+
21
+ const { label, description, readOnly, required, invalid, disabled, ...labelProps } =
22
+ $derived(getFieldContext());
23
+
24
+ const styles = tv({
25
+ base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200',
26
+ variants: {
27
+ shape: {
28
+ rectangle: 'rounded-none',
29
+ 'semi-round': '',
30
+ round: 'rounded-full',
31
+ },
32
+ padding: {
33
+ base: 'px-4 py-3',
34
+ round: 'px-5 py-3',
35
+ },
36
+ grow: {
37
+ true: 'resize-none',
38
+ false: '',
39
+ },
40
+ roundedSize: {
41
+ tiny: 'rounded-xl',
42
+ small: 'rounded-xl',
43
+ medium: 'rounded-2xl',
44
+ large: 'rounded-2xl',
45
+ giant: 'rounded-2xl',
46
+ },
47
+ textSize: {
48
+ tiny: 'text-xs',
49
+ small: 'text-sm',
50
+ medium: 'text-base',
51
+ large: 'text-lg',
52
+ giant: 'text-xl',
53
+ },
54
+ invalid: {
55
+ true: 'border-danger/80 border',
56
+ false: '',
57
+ },
58
+ },
59
+ });
60
+
61
+ const id = generateId();
62
+ const inputId = `input-${id}`;
63
+ const labelId = `label-${id}`;
64
+ const descriptionId = $derived(description ? `description-${id}` : undefined);
65
+
66
+ const onInput: FormEventHandler<HTMLTextAreaElement> = (event) => {
67
+ const element = event.target as HTMLTextAreaElement;
68
+ if (element && grow) {
69
+ element.style.height = 'auto';
70
+ element.style.height = `${element.scrollHeight}px`;
71
+ }
72
+
73
+ restProps?.oninput?.(event);
74
+ };
75
+ </script>
76
+
77
+ <div class="flex w-full flex-col gap-1" bind:this={containerRef}>
78
+ {#if label}
79
+ <Label id={labelId} for={inputId} {label} {...labelProps} />
80
+ {/if}
81
+
82
+ {#if description}
83
+ <Text color="secondary" size="small" id={descriptionId}>{description}</Text>
84
+ {/if}
85
+
86
+ <div class="relative w-full">
87
+ <textarea
88
+ oninput={onInput}
89
+ id={inputId}
90
+ aria-labelledby={label && labelId}
91
+ {required}
92
+ aria-required={required}
93
+ {disabled}
94
+ aria-disabled={disabled}
95
+ aria-describedby={descriptionId}
96
+ readonly={readOnly}
97
+ aria-readonly={readOnly}
98
+ class={cleanClass(
99
+ styles({
100
+ shape,
101
+ textSize: size,
102
+ padding: shape === 'round' ? 'round' : 'base',
103
+ grow,
104
+ roundedSize: shape === 'semi-round' ? size : undefined,
105
+ invalid,
106
+ }),
107
+ className,
108
+ )}
109
+ bind:this={ref}
110
+ bind:value
111
+ {...restProps}
112
+ ></textarea>
113
+ </div>
114
+ </div>
115
+
116
+ <style>
117
+ textarea::-ms-reveal {
118
+ display: none;
119
+ }
120
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { TextareaProps } from '../../types.js';
2
+ declare const Textarea: import("svelte").Component<TextareaProps, {}, "value" | "ref" | "containerRef">;
3
+ type Textarea = ReturnType<typeof Textarea>;
4
+ export default Textarea;
package/dist/index.d.ts CHANGED
@@ -56,6 +56,7 @@ export { default as VStack } from './components/Stack/VStack.svelte';
56
56
  export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
57
57
  export { default as Switch } from './components/Switch/Switch.svelte';
58
58
  export { default as Text } from './components/Text/Text.svelte';
59
+ export { default as Textarea } from './components/Textarea/Textarea.svelte';
59
60
  export { default as ThemeSwitcher } from './components/ThemeSwitcher/ThemeSwitcher.svelte';
60
61
  export * from './services/command-palette-manager.svelte.js';
61
62
  export * from './services/modal-manager.svelte.js';
package/dist/index.js CHANGED
@@ -58,6 +58,7 @@ export { default as VStack } from './components/Stack/VStack.svelte';
58
58
  export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
59
59
  export { default as Switch } from './components/Switch/Switch.svelte';
60
60
  export { default as Text } from './components/Text/Text.svelte';
61
+ export { default as Textarea } from './components/Textarea/Textarea.svelte';
61
62
  export { default as ThemeSwitcher } from './components/ThemeSwitcher/ThemeSwitcher.svelte';
62
63
  // helpers
63
64
  export * from './services/command-palette-manager.svelte.js';
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Translations } from './services/translation.svelte.js';
2
2
  import type { Snippet } from 'svelte';
3
- import type { HTMLAnchorAttributes, HTMLButtonAttributes, HTMLInputAttributes, HTMLLabelAttributes } from 'svelte/elements';
3
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes, HTMLInputAttributes, HTMLLabelAttributes, HTMLTextareaAttributes } from 'svelte/elements';
4
4
  export type Color = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
5
5
  export type TextColor = Color | 'muted';
6
6
  export type TextVariant = 'italic';
@@ -117,11 +117,22 @@ export type InputProps = BaseInputProps & {
117
117
  type?: HTMLInputAttributes['type'];
118
118
  leadingIcon?: IconLike | Snippet;
119
119
  trailingIcon?: IconLike | Snippet;
120
+ trailingText?: string;
120
121
  };
121
122
  export type PasswordInputProps = BaseInputProps & {
123
+ ref?: HTMLInputElement | null;
122
124
  translations?: TranslationProps<'show_password' | 'hide_password'>;
123
125
  isVisible?: boolean;
124
126
  };
127
+ export type TextareaProps = {
128
+ ref?: HTMLTextAreaElement | null;
129
+ containerRef?: HTMLElement | null;
130
+ class?: string;
131
+ value?: string;
132
+ size?: Size;
133
+ shape?: Shape;
134
+ grow?: boolean;
135
+ } & HTMLTextareaAttributes;
125
136
  export type SelectItem = {
126
137
  label?: string;
127
138
  value: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.28.1",
3
+ "version": "0.30.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "scripts": {
6
6
  "create": "node scripts/create.js",