@immich/ui 0.5.0 → 0.7.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 (42) hide show
  1. package/README.md +39 -45
  2. package/dist/common/context.svelte.d.ts +4 -0
  3. package/dist/common/context.svelte.js +6 -0
  4. package/dist/common/use-child.svelte.d.ts +5 -0
  5. package/dist/common/{use-context.svelte.js → use-child.svelte.js} +1 -1
  6. package/dist/components/Alert/Alert.svelte +8 -4
  7. package/dist/components/Alert/Alert.svelte.d.ts +2 -2
  8. package/dist/components/Card/Card.svelte +6 -6
  9. package/dist/components/Card/CardBody.svelte +3 -3
  10. package/dist/components/Card/CardFooter.svelte +2 -2
  11. package/dist/components/Card/CardHeader.svelte +2 -2
  12. package/dist/components/Form/Checkbox.svelte +4 -4
  13. package/dist/components/Form/Checkbox.svelte.d.ts +1 -1
  14. package/dist/components/Form/Field.svelte +29 -0
  15. package/dist/components/Form/Field.svelte.d.ts +6 -0
  16. package/dist/components/Form/HelperText.svelte +24 -0
  17. package/dist/components/Form/HelperText.svelte.d.ts +8 -0
  18. package/dist/components/Form/Input.svelte +109 -15
  19. package/dist/components/Form/Input.svelte.d.ts +2 -3
  20. package/dist/components/Form/PasswordInput.svelte +29 -0
  21. package/dist/components/Form/PasswordInput.svelte.d.ts +3 -0
  22. package/dist/components/LoadingSpinner/LoadingSpinner.svelte +54 -0
  23. package/dist/components/LoadingSpinner/LoadingSpinner.svelte.d.ts +7 -0
  24. package/dist/components/Stack/HStack.svelte +1 -1
  25. package/dist/components/Stack/HStack.svelte.d.ts +2 -1
  26. package/dist/components/Stack/Stack.svelte +0 -10
  27. package/dist/components/Stack/VStack.svelte +1 -1
  28. package/dist/components/Stack/VStack.svelte.d.ts +2 -1
  29. package/dist/components/Text/Text.svelte +1 -1
  30. package/dist/components/Text/Text.svelte.d.ts +1 -1
  31. package/dist/constants.d.ts +3 -1
  32. package/dist/constants.js +9 -7
  33. package/dist/index.d.ts +5 -1
  34. package/dist/index.js +5 -1
  35. package/dist/internal/Button.svelte +28 -7
  36. package/dist/internal/Child.svelte +4 -4
  37. package/dist/internal/Child.svelte.d.ts +3 -3
  38. package/dist/types.d.ts +28 -2
  39. package/dist/utils.d.ts +2 -2
  40. package/dist/utils.js +2 -0
  41. package/package.json +2 -2
  42. package/dist/common/use-context.svelte.d.ts +0 -5
package/README.md CHANGED
@@ -1,58 +1,52 @@
1
- # create-svelte
1
+ # @immich/ui
2
2
 
3
- Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
3
+ A component library for [Immich](https://immich.app), written in [Svelte](https://svelte.dev).
4
4
 
5
- Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
6
-
7
- ## Creating a project
8
-
9
- If you're seeing this, you've probably already done this step. Congrats!
5
+ ## Install
10
6
 
11
7
  ```bash
12
- # create a new project in the current directory
13
- npx sv create
14
-
15
- # create a new project in my-app
16
- npx sv create my-app
8
+ npm i -D @immich/ui
17
9
  ```
18
10
 
19
- ## Developing
20
-
21
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
22
-
23
- ```bash
24
- npm run dev
25
-
26
- # or start the server and open the app in a new browser tab
27
- npm run dev -- --open
11
+ ## Usage
12
+
13
+ Import components from `@immich/ui`. For example:
14
+
15
+ ```html
16
+ <script lang="ts">
17
+ import {
18
+ Card,
19
+ CardBody,
20
+ CardHeader,
21
+ CardTitle,
22
+ CardDescription,
23
+ Heading,
24
+ Text,
25
+ } from '@immich/ui';
26
+ </script>
27
+
28
+ <Card>
29
+ <CardHeader>
30
+ <CardTitle>@immich/ui</CardTitle>
31
+ <CardDescription>A component library</CardDescription>
32
+ </CardHeader>
33
+ <CardBody>
34
+ <Lorem />
35
+ </CardBody>
36
+ <CardFooter>Privacy should not be a luxury</CardFooter>
37
+ </Card>
28
38
  ```
29
39
 
30
- Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
40
+ ## Documentation
31
41
 
32
- ## Building
42
+ To view the examples located at `src/routes/examples`, run `npm start` and navigate to http://localhost:5173/.
33
43
 
34
- To build your library:
44
+ ## Contributing
35
45
 
36
- ```bash
37
- npm run package
38
- ```
46
+ PR's are welcome! Also feel free to reach out to the team on [Discord](https://discord.immich.app).
39
47
 
40
- To create a production version of your showcase app:
48
+ ## Technology
41
49
 
42
- ```bash
43
- npm run build
44
- ```
45
-
46
- You can preview the production build with `npm run preview`.
47
-
48
- > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
49
-
50
- ## Publishing
51
-
52
- Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
53
-
54
- To publish your library to [npm](https://www.npmjs.com):
55
-
56
- ```bash
57
- npm publish
58
- ```
50
+ - [Svelte](https://svelte.dev)
51
+ - [tailwindcss](https://tailwindcss.com)
52
+ - [Material Design icons (@mdi/js)](https://pictogrammers.com/library/mdi/)
@@ -0,0 +1,4 @@
1
+ import type { FieldContext } from '../types.js';
2
+ export declare const setFieldContext: (field: FieldContext) => FieldContext;
3
+ export declare const hasFieldContext: () => boolean;
4
+ export declare const getFieldContext: () => FieldContext;
@@ -0,0 +1,6 @@
1
+ import { withPrefix } from '../utils.js';
2
+ import { getContext, hasContext, setContext } from 'svelte';
3
+ const fieldKey = Symbol(withPrefix('field'));
4
+ export const setFieldContext = (field) => setContext(fieldKey, field);
5
+ export const hasFieldContext = () => hasContext(fieldKey);
6
+ export const getFieldContext = () => (getContext(fieldKey) || {});
@@ -0,0 +1,5 @@
1
+ import { ChildKey } from '../constants.js';
2
+ import { type Snippet } from 'svelte';
3
+ export declare const withChildrenSnippets: (key: ChildKey) => {
4
+ getChildren: (key: ChildKey) => Snippet<[]> | undefined;
5
+ };
@@ -1,4 +1,4 @@
1
- import { ContextKey } from '../constants.js';
1
+ import { ChildKey } from '../constants.js';
2
2
  import { withPrefix } from '../utils.js';
3
3
  import { setContext } from 'svelte';
4
4
  import { SvelteMap } from 'svelte/reactivity';
@@ -14,8 +14,8 @@
14
14
 
15
15
  type Props = {
16
16
  color?: Color;
17
- icon?: string;
18
- title: string;
17
+ icon?: string | false;
18
+ title?: string;
19
19
  children?: Snippet;
20
20
  };
21
21
 
@@ -27,7 +27,9 @@
27
27
  danger: mdiCloseCircleOutline,
28
28
  };
29
29
 
30
- const icon = $derived(iconOverride || (icons[color] ?? mdiInformationOutline));
30
+ const icon = $derived(
31
+ iconOverride === false ? undefined : iconOverride || (icons[color] ?? mdiInformationOutline),
32
+ );
31
33
  </script>
32
34
 
33
35
  <Card {color} variant="subtle">
@@ -40,7 +42,9 @@
40
42
  {/if}
41
43
 
42
44
  <div class="flex flex-col gap-2">
43
- <Text size="large" fontWeight={children ? 'bold' : undefined}>{title}</Text>
45
+ {#if title}
46
+ <Text size="large" fontWeight={children ? 'bold' : undefined}>{title}</Text>
47
+ {/if}
44
48
  {#if children}
45
49
  {@render children()}
46
50
  {/if}
@@ -2,8 +2,8 @@ import type { Color } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  declare const Alert: import("svelte").Component<{
4
4
  color?: Color;
5
- icon?: string;
6
- title: string;
5
+ icon?: string | false;
6
+ title?: string;
7
7
  children?: Snippet;
8
8
  }, {}, "">;
9
9
  export default Alert;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import { withChildrenSnippets } from '../../common/use-context.svelte.js';
2
+ import { withChildrenSnippets } from '../../common/use-child.svelte.js';
3
3
  import IconButton from '../IconButton/IconButton.svelte';
4
- import { ContextKey } from '../../constants.js';
4
+ import { ChildKey } from '../../constants.js';
5
5
  import type { Color, Shape } from '../../types.js';
6
6
  import { cleanClass } from '../../utils.js';
7
7
  import { mdiChevronDown } from '@mdi/js';
@@ -104,10 +104,10 @@
104
104
  expanded = !expanded;
105
105
  };
106
106
 
107
- const { getChildren: getChildSnippet } = withChildrenSnippets(ContextKey.Card);
108
- const headerChildren = $derived(getChildSnippet(ContextKey.CardHeader));
109
- const bodyChildren = $derived(getChildSnippet(ContextKey.CardBody));
110
- const footerChildren = $derived(getChildSnippet(ContextKey.CardFooter));
107
+ const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Card);
108
+ const headerChildren = $derived(getChildSnippet(ChildKey.CardHeader));
109
+ const bodyChildren = $derived(getChildSnippet(ChildKey.CardBody));
110
+ const footerChildren = $derived(getChildSnippet(ChildKey.CardFooter));
111
111
 
112
112
  const headerClasses = 'flex flex-col space-y-1.5';
113
113
  const headerContainerClasses = $derived(
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { ContextKey } from '../../constants.js';
2
+ import { ChildKey } from '../../constants.js';
3
3
  import Child from '../../internal/Child.svelte';
4
4
  import { cleanClass } from '../../utils.js';
5
5
  import type { Snippet } from 'svelte';
@@ -13,8 +13,8 @@
13
13
  let { class: className, children }: Props = $props();
14
14
  </script>
15
15
 
16
- <Child for={ContextKey.Card} as={ContextKey.CardBody}>
17
- <div class={twMerge(cleanClass('p-4', className))}>
16
+ <Child for={ChildKey.Card} as={ChildKey.CardBody}>
17
+ <div class={twMerge(cleanClass('w-full p-4', className))}>
18
18
  {@render children?.()}
19
19
  </div>
20
20
  </Child>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { ContextKey } from '../../constants.js';
2
+ import { ChildKey } from '../../constants.js';
3
3
  import Child from '../../internal/Child.svelte';
4
4
  import type { Snippet } from 'svelte';
5
5
 
@@ -10,6 +10,6 @@
10
10
  let { children }: Props = $props();
11
11
  </script>
12
12
 
13
- <Child for={ContextKey.Card} as={ContextKey.CardFooter}>
13
+ <Child for={ChildKey.Card} as={ChildKey.CardFooter}>
14
14
  {@render children?.()}
15
15
  </Child>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { ContextKey } from '../../constants.js';
2
+ import { ChildKey } from '../../constants.js';
3
3
  import Child from '../../internal/Child.svelte';
4
4
  import type { Snippet } from 'svelte';
5
5
 
@@ -10,6 +10,6 @@
10
10
  let { children }: Props = $props();
11
11
  </script>
12
12
 
13
- <Child for={ContextKey.Card} as={ContextKey.CardHeader}>
13
+ <Child for={ChildKey.Card} as={ChildKey.CardHeader}>
14
14
  {@render children?.()}
15
15
  </Child>
@@ -81,12 +81,12 @@
81
81
  bind:checked
82
82
  {...restProps}
83
83
  >
84
- {#snippet children({ checked })}
84
+ {#snippet children({ checked, indeterminate })}
85
85
  <div class={cleanClass('flex items-center justify-center text-current')}>
86
- {#if checked === true}
87
- <Icon icon={mdiCheck} size="100%" class={cleanClass(icon({ color }))} />
88
- {:else if checked === 'indeterminate'}
86
+ {#if indeterminate}
89
87
  <Icon icon={mdiMinus} size="100%" class={cleanClass(icon({ color }))} />
88
+ {:else if checked}
89
+ <Icon icon={mdiCheck} size="100%" class={cleanClass(icon({ color }))} />
90
90
  {/if}
91
91
  </div>
92
92
  {/snippet}
@@ -4,5 +4,5 @@ declare const Checkbox: import("svelte").Component<Omit<Omit<CheckboxPrimitive.R
4
4
  color?: Color;
5
5
  shape?: Shape;
6
6
  size?: Size;
7
- }, {}, "ref" | "checked">;
7
+ }, {}, "checked" | "ref">;
8
8
  export default Checkbox;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { setFieldContext } from '../../common/context.svelte.js';
3
+ import { withChildrenSnippets } from '../../common/use-child.svelte.js';
4
+ import { ChildKey } from '../../constants.js';
5
+ import type { FieldContext } from '../../types.js';
6
+ import { type Snippet } from 'svelte';
7
+
8
+ type Props = FieldContext & {
9
+ children: Snippet;
10
+ };
11
+
12
+ const { children, ...props }: Props = $props();
13
+
14
+ const state = $state(props);
15
+
16
+ setFieldContext(state);
17
+
18
+ const { getChildren: getChildSnippet } = withChildrenSnippets(ChildKey.Field);
19
+ const helperTextChildren = $derived(getChildSnippet(ChildKey.HelperText));
20
+ </script>
21
+
22
+ <div>
23
+ {@render children()}
24
+ {#if helperTextChildren}
25
+ <div class="pt-1">
26
+ {@render helperTextChildren?.()}
27
+ </div>
28
+ {/if}
29
+ </div>
@@ -0,0 +1,6 @@
1
+ import type { FieldContext } from '../../types.js';
2
+ import { type Snippet } from 'svelte';
3
+ declare const Field: import("svelte").Component<FieldContext & {
4
+ children: Snippet;
5
+ }, {}, "">;
6
+ export default Field;
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ import Text from '../Text/Text.svelte';
3
+ import { ChildKey } from '../../constants.js';
4
+ import Child from '../../internal/Child.svelte';
5
+ import type { Color } from '../../types.js';
6
+ import { cleanClass } from '../../utils.js';
7
+ import type { Snippet } from 'svelte';
8
+
9
+ type Props = {
10
+ color?: Color;
11
+ class?: string;
12
+ children?: Snippet;
13
+ };
14
+
15
+ let { class: className, children, color }: Props = $props();
16
+ </script>
17
+
18
+ <Child for={ChildKey.Field} as={ChildKey.HelperText}>
19
+ <div class={cleanClass(className)}>
20
+ <Text {color} size="small">
21
+ {@render children?.()}
22
+ </Text>
23
+ </div>
24
+ </Child>
@@ -0,0 +1,8 @@
1
+ import type { Color } from '../../types.js';
2
+ import type { Snippet } from 'svelte';
3
+ declare const HelperText: import("svelte").Component<{
4
+ color?: Color;
5
+ class?: string;
6
+ children?: Snippet;
7
+ }, {}, "">;
8
+ export default HelperText;
@@ -1,22 +1,116 @@
1
1
  <script lang="ts">
2
- import type { HTMLInputAttributes } from 'svelte/elements';
3
- import type { WithElementRef } from 'bits-ui';
4
- import { cleanClass } from '../../utils.js';
2
+ import { getFieldContext } from '../../common/context.svelte.js';
3
+ import type { InputProps } from '../../types.js';
4
+ import { cleanClass, generateId } from '../../utils.js';
5
+ import { tv } from 'tailwind-variants';
5
6
 
6
7
  let {
7
- ref = $bindable(null),
8
- value = $bindable(),
8
+ shape = 'semi-round',
9
+ size = 'medium',
9
10
  class: className,
11
+ value = $bindable<string>(),
12
+ trailingIcon,
10
13
  ...restProps
11
- }: WithElementRef<HTMLInputAttributes> = $props();
14
+ }: InputProps = $props();
15
+
16
+ const {
17
+ label,
18
+ readOnly = false,
19
+ required = false,
20
+ invalid = false,
21
+ disabled = false,
22
+ } = $derived(getFieldContext());
23
+
24
+ const labelStyles = tv({
25
+ base: '',
26
+ variants: {
27
+ size: {
28
+ tiny: 'text-xs',
29
+ small: 'text-sm',
30
+ medium: 'text-md',
31
+ large: 'text-lg',
32
+ giant: 'text-xl',
33
+ },
34
+ },
35
+ });
36
+
37
+ const inputStyles = tv({
38
+ base: 'w-full outline-none disabled:cursor-not-allowed bg-gray-200 dark:bg-gray-600 disabled:bg-gray-300 disabled:text-gray-200 dark:disabled:bg-gray-800 aria-readonly:text-dark/50 dark:aria-readonly:text-dark/75',
39
+ variants: {
40
+ shape: {
41
+ rectangle: 'rounded-none',
42
+ 'semi-round': '',
43
+ round: 'rounded-full',
44
+ },
45
+ padding: {
46
+ base: 'px-3 py-2',
47
+ round: 'px-4 py-2',
48
+ },
49
+ roundedSize: {
50
+ tiny: 'rounded-xl',
51
+ small: 'rounded-xl',
52
+ medium: 'rounded-2xl',
53
+ large: 'rounded-2xl',
54
+ giant: 'rounded-2xl',
55
+ },
56
+ textSize: {
57
+ tiny: 'text-xs',
58
+ small: 'text-sm',
59
+ medium: 'text-md',
60
+ large: 'text-lg',
61
+ giant: 'text-xl',
62
+ },
63
+ invalid: {
64
+ true: 'border border-danger/80',
65
+ false: '',
66
+ },
67
+ },
68
+ });
69
+
70
+ const id = generateId();
71
+ const inputId = `input-${id}`;
72
+ const labelId = `label-${id}`;
12
73
  </script>
13
74
 
14
- <input
15
- bind:this={ref}
16
- class={cleanClass(
17
- 'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
18
- className,
19
- )}
20
- bind:value
21
- {...restProps}
22
- />
75
+ <div class="flex flex-col gap-1">
76
+ {#if label}
77
+ <label id={labelId} for={inputId} class={labelStyles({ size })}>{label}</label>
78
+ {/if}
79
+
80
+ <div class="relative">
81
+ <input
82
+ id={label && inputId}
83
+ aria-labelledby={label && labelId}
84
+ {required}
85
+ aria-required={required}
86
+ {disabled}
87
+ aria-disabled={disabled}
88
+ readonly={readOnly}
89
+ aria-readonly={readOnly}
90
+ class={cleanClass(
91
+ inputStyles({
92
+ shape,
93
+ textSize: size,
94
+ padding: shape === 'round' ? 'round' : 'base',
95
+ roundedSize: shape === 'semi-round' ? size : undefined,
96
+ invalid,
97
+ }),
98
+ trailingIcon && '!pr-10',
99
+ className,
100
+ )}
101
+ bind:value
102
+ {...restProps}
103
+ />
104
+ {#if trailingIcon}
105
+ <div tabindex="-1" class={cleanClass('absolute inset-y-0 end-0 flex items-center')}>
106
+ {@render trailingIcon()}
107
+ </div>
108
+ {/if}
109
+ </div>
110
+ </div>
111
+
112
+ <style>
113
+ input::-ms-reveal {
114
+ display: none;
115
+ }
116
+ </style>
@@ -1,4 +1,3 @@
1
- import type { HTMLInputAttributes } from 'svelte/elements';
2
- import type { WithElementRef } from 'bits-ui';
3
- declare const Input: import("svelte").Component<WithElementRef<HTMLInputAttributes>, {}, "value" | "ref">;
1
+ import type { InputProps } from '../../types.js';
2
+ declare const Input: import("svelte").Component<InputProps, {}, "value">;
4
3
  export default Input;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import { IconButton, Input, type PasswordInputProps } from '../../index.js';
3
+ import { mdiEyeOffOutline, mdiEyeOutline } from '@mdi/js';
4
+
5
+ let {
6
+ value = $bindable<string>(),
7
+ showLabel = 'Show password',
8
+ hideLabel = 'Hide password',
9
+ isVisible = $bindable<boolean>(false),
10
+ color = 'secondary',
11
+ ...props
12
+ }: PasswordInputProps = $props();
13
+ </script>
14
+
15
+ <Input bind:value type={isVisible ? 'text' : 'password'} {color} {...props}>
16
+ {#snippet trailingIcon()}
17
+ {#if value?.length > 0}
18
+ <IconButton
19
+ variant="ghost"
20
+ shape="round"
21
+ color="secondary"
22
+ class="m-1"
23
+ icon={isVisible ? mdiEyeOffOutline : mdiEyeOutline}
24
+ onclick={() => (isVisible = !isVisible)}
25
+ title={isVisible ? hideLabel : showLabel}
26
+ ></IconButton>
27
+ {/if}
28
+ {/snippet}
29
+ </Input>
@@ -0,0 +1,3 @@
1
+ import { type PasswordInputProps } from '../../index.js';
2
+ declare const PasswordInput: import("svelte").Component<PasswordInputProps, {}, "value" | "isVisible">;
3
+ export default PasswordInput;
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import type { Color, Size } from '../../types.js';
3
+ import { cleanClass } from '../../utils.js';
4
+ import { tv } from 'tailwind-variants';
5
+
6
+ interface Props {
7
+ size?: Size;
8
+ color?: Color;
9
+ class?: string;
10
+ }
11
+
12
+ let { size = 'medium', color = 'primary', class: className }: Props = $props();
13
+
14
+ const styles = tv({
15
+ base: 'animate-spin fill-primary text-gray-400 dark:text-gray-600',
16
+ variants: {
17
+ size: {
18
+ tiny: 'h-3',
19
+ small: 'h-4',
20
+ medium: 'h-5',
21
+ large: 'h-6',
22
+ giant: 'h-12',
23
+ },
24
+ color: {
25
+ primary: 'fill-primary',
26
+ secondary: 'fill-dark',
27
+ success: 'fill-success',
28
+ danger: 'fill-danger',
29
+ warning: 'fill-warning',
30
+ info: 'fill-info',
31
+ },
32
+ },
33
+ });
34
+ </script>
35
+
36
+ <div>
37
+ <svg
38
+ role="status"
39
+ class={cleanClass(styles({ color, size }), className)}
40
+ viewBox="0 0 100 101"
41
+ fill="none"
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ data-testid="loading-spinner"
44
+ >
45
+ <path
46
+ d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
47
+ fill="currentColor"
48
+ />
49
+ <path
50
+ d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
51
+ fill="currentFill"
52
+ />
53
+ </svg>
54
+ </div>
@@ -0,0 +1,7 @@
1
+ import type { Color, Size } from '../../types.js';
2
+ declare const LoadingSpinner: import("svelte").Component<{
3
+ size?: Size;
4
+ color?: Color;
5
+ class?: string;
6
+ }, {}, "">;
7
+ export default LoadingSpinner;
@@ -5,6 +5,6 @@
5
5
  const { class: className, children, ...props }: HStackProps = $props();
6
6
  </script>
7
7
 
8
- <Stack direction="row" class={className} {...props}>
8
+ <Stack direction="row" align="center" class={className} {...props}>
9
9
  {@render children()}
10
10
  </Stack>
@@ -1,8 +1,9 @@
1
1
  declare const HStack: import("svelte").Component<{
2
2
  class?: string;
3
3
  children: import("svelte").Snippet;
4
- align?: "start" | "center" | "end";
5
4
  gap?: import("../../types.js").Gap;
6
5
  wrap?: boolean;
6
+ fullWidth?: boolean;
7
+ fullHeight?: boolean;
7
8
  }, {}, "">;
8
9
  export default HStack;
@@ -24,14 +24,6 @@
24
24
  center: 'items-center',
25
25
  end: 'items-end',
26
26
  },
27
- fullWidth: {
28
- true: 'w-full',
29
- false: '',
30
- },
31
- fullHeight: {
32
- true: 'h-full',
33
- false: '',
34
- },
35
27
  gap: {
36
28
  0: 'gap-0',
37
29
  1: 'gap-1',
@@ -58,8 +50,6 @@
58
50
  direction,
59
51
  gap,
60
52
  wrap,
61
- fullWidth: direction === 'row',
62
- fullHeight: direction === 'column',
63
53
  }),
64
54
  className,
65
55
  )}
@@ -5,6 +5,6 @@
5
5
  const { class: className, children, ...props }: VStackProps = $props();
6
6
  </script>
7
7
 
8
- <Stack direction="column" class={className} {...props}>
8
+ <Stack direction="column" align="center" class={className} {...props}>
9
9
  {@render children()}
10
10
  </Stack>
@@ -1,8 +1,9 @@
1
1
  declare const VStack: import("svelte").Component<{
2
2
  class?: string;
3
3
  children: import("svelte").Snippet;
4
- align?: "start" | "center" | "end";
5
4
  gap?: import("../../types.js").Gap;
6
5
  wrap?: boolean;
6
+ fullWidth?: boolean;
7
+ fullHeight?: boolean;
7
8
  }, {}, "">;
8
9
  export default VStack;
@@ -6,11 +6,11 @@
6
6
 
7
7
  type Props = {
8
8
  color?: Color;
9
+ class?: string;
9
10
  size?: Size;
10
11
  children: Snippet;
11
12
  variant?: 'italic';
12
13
  fontWeight?: 'light' | 'normal' | 'semi-bold' | 'bold';
13
- class?: string;
14
14
  };
15
15
 
16
16
  const { color, size, fontWeight = 'normal', children, class: className }: Props = $props();
@@ -2,10 +2,10 @@ import type { Color, Size } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  declare const Text: import("svelte").Component<{
4
4
  color?: Color;
5
+ class?: string;
5
6
  size?: Size;
6
7
  children: Snippet;
7
8
  variant?: "italic";
8
9
  fontWeight?: "light" | "normal" | "semi-bold" | "bold";
9
- class?: string;
10
10
  }, {}, "">;
11
11
  export default Text;
@@ -1,4 +1,6 @@
1
- export declare enum ContextKey {
1
+ export declare enum ChildKey {
2
+ Field = "field",
3
+ HelperText = "helped-text",
2
4
  Card = "card",
3
5
  CardHeader = "card-header",
4
6
  CardBody = "card-body",
package/dist/constants.js CHANGED
@@ -1,7 +1,9 @@
1
- export var ContextKey;
2
- (function (ContextKey) {
3
- ContextKey["Card"] = "card";
4
- ContextKey["CardHeader"] = "card-header";
5
- ContextKey["CardBody"] = "card-body";
6
- ContextKey["CardFooter"] = "card-footer";
7
- })(ContextKey || (ContextKey = {}));
1
+ export var ChildKey;
2
+ (function (ChildKey) {
3
+ ChildKey["Field"] = "field";
4
+ ChildKey["HelperText"] = "helped-text";
5
+ ChildKey["Card"] = "card";
6
+ ChildKey["CardHeader"] = "card-header";
7
+ ChildKey["CardBody"] = "card-body";
8
+ ChildKey["CardFooter"] = "card-footer";
9
+ })(ChildKey || (ChildKey = {}));
package/dist/index.d.ts CHANGED
@@ -8,16 +8,20 @@ export { default as CardHeader } from './components/Card/CardHeader.svelte';
8
8
  export { default as CardTitle } from './components/Card/CardTitle.svelte';
9
9
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
10
10
  export { default as Checkbox } from './components/Form/Checkbox.svelte';
11
+ export { default as Field } from './components/Form/Field.svelte';
12
+ export { default as HelperText } from './components/Form/HelperText.svelte';
11
13
  export { default as Input } from './components/Form/Input.svelte';
12
14
  export { default as Label } from './components/Form/Label.svelte';
15
+ export { default as PasswordInput } from './components/Form/PasswordInput.svelte';
13
16
  export { default as Heading } from './components/Heading/Heading.svelte';
14
17
  export { default as Icon } from './components/Icon/Icon.svelte';
15
18
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
16
19
  export { default as Link } from './components/Link/Link.svelte';
17
- export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
20
+ export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
18
21
  export { default as Logo } from './components/Logo/Logo.svelte';
19
22
  export { default as HStack } from './components/Stack/HStack.svelte';
20
23
  export { default as Stack } from './components/Stack/Stack.svelte';
21
24
  export { default as VStack } from './components/Stack/VStack.svelte';
25
+ export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
22
26
  export { default as Text } from './components/Text/Text.svelte';
23
27
  export * from './types.js';
package/dist/index.js CHANGED
@@ -8,16 +8,20 @@ export { default as CardHeader } from './components/Card/CardHeader.svelte';
8
8
  export { default as CardTitle } from './components/Card/CardTitle.svelte';
9
9
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
10
10
  export { default as Checkbox } from './components/Form/Checkbox.svelte';
11
+ export { default as Field } from './components/Form/Field.svelte';
12
+ export { default as HelperText } from './components/Form/HelperText.svelte';
11
13
  export { default as Input } from './components/Form/Input.svelte';
12
14
  export { default as Label } from './components/Form/Label.svelte';
15
+ export { default as PasswordInput } from './components/Form/PasswordInput.svelte';
13
16
  export { default as Heading } from './components/Heading/Heading.svelte';
14
17
  export { default as Icon } from './components/Icon/Icon.svelte';
15
18
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
16
19
  export { default as Link } from './components/Link/Link.svelte';
17
- export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
20
+ export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
18
21
  export { default as Logo } from './components/Logo/Logo.svelte';
19
22
  export { default as HStack } from './components/Stack/HStack.svelte';
20
23
  export { default as Stack } from './components/Stack/Stack.svelte';
21
24
  export { default as VStack } from './components/Stack/VStack.svelte';
25
+ export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
22
26
  export { default as Text } from './components/Text/Text.svelte';
23
27
  export * from './types.js';
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
- import type { ButtonProps } from '../types.js';
2
+ import type { ButtonProps, Size } from '../types.js';
3
3
  import { cleanClass } from '../utils.js';
4
+ import { LoadingSpinner } from '../index.js';
4
5
  import { Button as ButtonPrimitive } from 'bits-ui';
5
6
  import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
6
7
  import { twMerge } from 'tailwind-merge';
@@ -18,6 +19,7 @@
18
19
  color = 'primary',
19
20
  shape = 'semi-round',
20
21
  size = 'medium',
22
+ loading = false,
21
23
  fullWidth = false,
22
24
  icon = false,
23
25
  class: className = '',
@@ -25,6 +27,8 @@
25
27
  ...restProps
26
28
  }: InternalButtonProps = $props();
27
29
 
30
+ const disabled = $derived((restProps as HTMLButtonAttributes).disabled || loading);
31
+
28
32
  const buttonVariants = tv({
29
33
  base: 'ring-offset-background focus-visible:ring-ring flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
30
34
  variants: {
@@ -58,11 +62,11 @@
58
62
  giant: 'h-12 w-12 text-lg',
59
63
  },
60
64
  roundedSize: {
61
- tiny: 'rounded-md',
62
- small: 'rounded-md',
63
- medium: 'rounded-lg',
64
- large: 'rounded-lg',
65
- giant: 'rounded-lg',
65
+ tiny: 'rounded-lg',
66
+ small: 'rounded-lg',
67
+ medium: 'rounded-xl',
68
+ large: 'rounded-xl',
69
+ giant: 'rounded-2xl',
66
70
  },
67
71
  filledColor: {
68
72
  primary: 'bg-primary text-light hover:bg-primary/80',
@@ -99,6 +103,14 @@
99
103
  },
100
104
  });
101
105
 
106
+ const spinnerSizes: Record<Size, Size> = {
107
+ tiny: 'tiny',
108
+ small: 'tiny',
109
+ medium: 'small',
110
+ large: 'medium',
111
+ giant: 'large',
112
+ };
113
+
102
114
  const classList = $derived(
103
115
  cleanClass(
104
116
  twMerge(
@@ -129,7 +141,16 @@
129
141
  class={classList}
130
142
  type={type as HTMLButtonAttributes['type']}
131
143
  {...restProps as HTMLButtonAttributes}
144
+ {disabled}
145
+ aria-disabled={disabled}
132
146
  >
133
- {@render children?.()}
147
+ {#if loading}
148
+ <div class="flex items-center justify-center gap-2">
149
+ <LoadingSpinner {color} size={spinnerSizes[size]} />
150
+ {@render children?.()}
151
+ </div>
152
+ {:else}
153
+ {@render children?.()}
154
+ {/if}
134
155
  </ButtonPrimitive.Root>
135
156
  {/if}
@@ -1,12 +1,12 @@
1
1
  <script lang="ts">
2
- import { ContextKey } from '../constants.js';
2
+ import { ChildKey } from '../constants.js';
3
3
  import { withPrefix } from '../utils.js';
4
4
  import { getContext, type Snippet } from 'svelte';
5
5
 
6
- type ContextType = { register: (key: ContextKey, snippet: Snippet) => void };
6
+ type ContextType = { register: (key: ChildKey, snippet: Snippet) => void };
7
7
  type Props = {
8
- for: ContextKey;
9
- as: ContextKey;
8
+ for: ChildKey;
9
+ as: ChildKey;
10
10
  children: Snippet;
11
11
  };
12
12
 
@@ -1,8 +1,8 @@
1
- import { ContextKey } from '../constants.js';
1
+ import { ChildKey } from '../constants.js';
2
2
  import { type Snippet } from 'svelte';
3
3
  declare const Child: import("svelte").Component<{
4
- for: ContextKey;
5
- as: ContextKey;
4
+ for: ChildKey;
5
+ as: ChildKey;
6
6
  children: Snippet;
7
7
  }, {}, "">;
8
8
  export default Child;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
2
+ import type { HTMLAnchorAttributes, HTMLButtonAttributes, HTMLInputAttributes } from 'svelte/elements';
3
3
  export type Color = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info';
4
4
  export type Size = 'tiny' | 'small' | 'medium' | 'large' | 'giant';
5
5
  export type HeadingSize = Size | 'title';
@@ -37,17 +37,43 @@ export type IconProps = {
37
37
  export type IconButtonProps = ButtonBaseProps & IconProps;
38
38
  export type ButtonProps = ButtonBaseProps & {
39
39
  fullWidth?: boolean;
40
+ loading?: boolean;
40
41
  };
41
42
  type StackBaseProps = {
42
43
  class?: string;
43
44
  children: Snippet;
44
- align?: 'start' | 'center' | 'end';
45
45
  gap?: Gap;
46
46
  wrap?: boolean;
47
+ fullWidth?: boolean;
48
+ fullHeight?: boolean;
47
49
  };
48
50
  export type StackProps = StackBaseProps & {
51
+ align?: 'start' | 'center' | 'end';
49
52
  direction?: 'row' | 'column';
50
53
  };
51
54
  export type HStackProps = StackBaseProps;
52
55
  export type VStackProps = StackBaseProps;
56
+ export type FieldContext = {
57
+ label?: string;
58
+ invalid?: boolean;
59
+ disabled?: boolean;
60
+ required?: boolean;
61
+ readOnly?: boolean;
62
+ };
63
+ type BaseInputProps = {
64
+ class?: string;
65
+ value?: string;
66
+ size?: Size;
67
+ shape?: Shape;
68
+ inputSize?: HTMLInputAttributes['size'];
69
+ } & Omit<HTMLInputAttributes, 'size' | 'type'>;
70
+ export type InputProps = BaseInputProps & {
71
+ type?: HTMLInputAttributes['type'];
72
+ trailingIcon?: Snippet;
73
+ };
74
+ export type PasswordInputProps = BaseInputProps & {
75
+ showLabel?: string;
76
+ hideLabel?: string;
77
+ isVisible?: boolean;
78
+ };
53
79
  export {};
package/dist/utils.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import type { ContextKey } from './constants.js';
2
1
  export declare const cleanClass: (...classNames: unknown[]) => string;
3
- export declare const withPrefix: (key: ContextKey) => string;
2
+ export declare const withPrefix: (key: string) => string;
3
+ export declare const generateId: () => string;
package/dist/utils.js CHANGED
@@ -9,3 +9,5 @@ export const cleanClass = (...classNames) => {
9
9
  .join(' ');
10
10
  };
11
11
  export const withPrefix = (key) => `immich-ui-${key}`;
12
+ let _count = 0;
13
+ export const generateId = () => `id-${_count++}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "scripts": {
6
6
  "start": "npm run start:dev",
@@ -57,7 +57,7 @@
57
57
  "svelte-check": "^4.0.0",
58
58
  "tailwindcss": "^3.4.9",
59
59
  "typescript": "^5.0.0",
60
- "typescript-eslint": "^8.0.0",
60
+ "typescript-eslint": "^8.15.0",
61
61
  "vite": "^5.0.11",
62
62
  "vitest": "^2.0.4"
63
63
  },
@@ -1,5 +0,0 @@
1
- import { ContextKey } from '../constants.js';
2
- import { type Snippet } from 'svelte';
3
- export declare const withChildrenSnippets: (key: ContextKey) => {
4
- getChildren: (key: ContextKey) => Snippet<[]> | undefined;
5
- };