@immich/ui 0.6.0 → 0.8.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 (64) hide show
  1. package/README.md +39 -45
  2. package/dist/assets/appstore-badge.svg +46 -0
  3. package/dist/assets/fdroid-badge.svg +124 -0
  4. package/dist/assets/playstore-badge.png +0 -0
  5. package/dist/components/Alert/Alert.svelte +13 -7
  6. package/dist/components/Alert/Alert.svelte.d.ts +3 -2
  7. package/dist/components/AppShell/AppShell.svelte +34 -0
  8. package/dist/components/AppShell/AppShell.svelte.d.ts +6 -0
  9. package/dist/components/AppShell/AppShellHeader.svelte +15 -0
  10. package/dist/components/AppShell/AppShellHeader.svelte.d.ts +5 -0
  11. package/dist/components/AppShell/AppShellSidebar.svelte +23 -0
  12. package/dist/components/AppShell/AppShellSidebar.svelte.d.ts +7 -0
  13. package/dist/components/AppShell/PageLayout.svelte +44 -0
  14. package/dist/components/AppShell/PageLayout.svelte.d.ts +9 -0
  15. package/dist/components/Avatar/Avatar.svelte +66 -0
  16. package/dist/components/Avatar/Avatar.svelte.d.ts +7 -0
  17. package/dist/components/Card/Card.svelte +7 -9
  18. package/dist/components/Card/CardBody.svelte +1 -1
  19. package/dist/components/Card/CardFooter.svelte +6 -2
  20. package/dist/components/Card/CardFooter.svelte.d.ts +1 -0
  21. package/dist/components/Code/Code.svelte +62 -0
  22. package/dist/components/Code/Code.svelte.d.ts +9 -0
  23. package/dist/components/Form/Checkbox.svelte +67 -27
  24. package/dist/components/Form/Checkbox.svelte.d.ts +2 -2
  25. package/dist/components/Form/Input.svelte +43 -35
  26. package/dist/components/Form/Input.svelte.d.ts +2 -9
  27. package/dist/components/Form/PasswordInput.svelte +31 -0
  28. package/dist/components/Form/PasswordInput.svelte.d.ts +3 -0
  29. package/dist/components/FormatBytes/FormatBytes.svelte +16 -0
  30. package/dist/components/FormatBytes/FormatBytes.svelte.d.ts +6 -0
  31. package/dist/components/Heading/Heading.svelte +2 -1
  32. package/dist/components/Heading/Heading.svelte.d.ts +1 -1
  33. package/dist/components/LoadingSpinner/LoadingSpinner.svelte +54 -0
  34. package/dist/components/LoadingSpinner/LoadingSpinner.svelte.d.ts +7 -0
  35. package/dist/components/Logo/Logo.svelte +8 -8
  36. package/dist/components/Logo/Logo.svelte.d.ts +1 -2
  37. package/dist/components/MultiSelect/MultiSelect.svelte +15 -0
  38. package/dist/components/MultiSelect/MultiSelect.svelte.d.ts +3 -0
  39. package/dist/components/Navbar/NavbarGroup.svelte +12 -0
  40. package/dist/components/Navbar/NavbarGroup.svelte.d.ts +4 -0
  41. package/dist/components/Navbar/NavbarItem.svelte +30 -0
  42. package/dist/components/Navbar/NavbarItem.svelte.d.ts +7 -0
  43. package/dist/components/Scrollable/Scrollable.svelte +41 -0
  44. package/dist/components/Scrollable/Scrollable.svelte.d.ts +6 -0
  45. package/dist/components/Select/Select.svelte +15 -0
  46. package/dist/components/Select/Select.svelte.d.ts +3 -0
  47. package/dist/components/Switch/Switch.svelte +99 -0
  48. package/dist/components/Switch/Switch.svelte.d.ts +10 -0
  49. package/dist/components/Text/Text.svelte +15 -3
  50. package/dist/components/Text/Text.svelte.d.ts +1 -1
  51. package/dist/constants.d.ts +3 -0
  52. package/dist/constants.js +3 -0
  53. package/dist/index.d.ts +25 -0
  54. package/dist/index.js +28 -0
  55. package/dist/internal/Button.svelte +28 -16
  56. package/dist/internal/Select.svelte +174 -0
  57. package/dist/internal/Select.svelte.d.ts +9 -0
  58. package/dist/services/theme.svelte.d.ts +5 -0
  59. package/dist/services/theme.svelte.js +13 -0
  60. package/dist/types.d.ts +45 -2
  61. package/dist/types.js +5 -1
  62. package/dist/utilities/byte-units.d.ts +52 -0
  63. package/dist/utilities/byte-units.js +75 -0
  64. package/package.json +7 -5
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import { cleanClass } from '../../utils.js';
3
+ import type { Snippet } from 'svelte';
4
+
5
+ type Props = {
6
+ class?: string;
7
+ children?: Snippet;
8
+ };
9
+
10
+ const { class: className, children }: Props = $props();
11
+ </script>
12
+
13
+ <div class={cleanClass('immich-scrollbar h-full w-full overflow-auto', className)}>
14
+ {@render children?.()}
15
+ </div>
16
+
17
+ <style>
18
+ /* width */
19
+ .immich-scrollbar::-webkit-scrollbar {
20
+ width: 8px;
21
+ height: 8px;
22
+ }
23
+
24
+ /* Track */
25
+ .immich-scrollbar::-webkit-scrollbar-track {
26
+ background: #f1f1f1;
27
+ border-radius: 16px;
28
+ }
29
+
30
+ /* Handle */
31
+ .immich-scrollbar::-webkit-scrollbar-thumb {
32
+ background: rgba(85, 86, 87, 0.408);
33
+ border-radius: 16px;
34
+ }
35
+
36
+ /* Handle on hover */
37
+ .immich-scrollbar::-webkit-scrollbar-thumb:hover {
38
+ background: #4250afad;
39
+ border-radius: 16px;
40
+ }
41
+ </style>
@@ -0,0 +1,6 @@
1
+ import type { Snippet } from 'svelte';
2
+ declare const Scrollable: import("svelte").Component<{
3
+ class?: string;
4
+ children?: Snippet;
5
+ }, {}, "">;
6
+ export default Scrollable;
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import InternalSelect from '../../internal/Select.svelte';
3
+ import type { SelectItem, SelectProps } from '../../types.js';
4
+
5
+ type T = SelectItem;
6
+
7
+ let { value = $bindable(), onChange, ...restProps }: SelectProps<T> = $props();
8
+
9
+ const handleChange = (items: T[]) => {
10
+ value = items[0] as T;
11
+ onChange?.(value);
12
+ };
13
+ </script>
14
+
15
+ <InternalSelect onChange={handleChange} {...restProps} />
@@ -0,0 +1,3 @@
1
+ import type { SelectItem, SelectProps } from '../../types.js';
2
+ declare const Select: import("svelte").Component<SelectProps<SelectItem>, {}, "value">;
3
+ export default Select;
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import { getFieldContext } from '../../common/context.svelte.js';
3
+ import type { Color } from '../../types.js';
4
+ import { cleanClass } from '../../utils.js';
5
+ import type { HTMLInputAttributes } from 'svelte/elements';
6
+ import { tv } from 'tailwind-variants';
7
+
8
+ type Props = {
9
+ checked?: boolean;
10
+ color?: Color;
11
+ disabled?: boolean;
12
+ class?: string;
13
+ onToggle?: ((checked: boolean) => void) | undefined;
14
+ } & HTMLInputAttributes;
15
+
16
+ let {
17
+ checked = $bindable(false),
18
+ class: className,
19
+ color = 'primary',
20
+ onToggle = undefined,
21
+ ...restProps
22
+ }: Props = $props();
23
+
24
+ const {
25
+ label,
26
+ readOnly = false,
27
+ required = false,
28
+ disabled = false,
29
+ } = $derived(getFieldContext());
30
+
31
+ const enabled = $derived(checked && !disabled);
32
+
33
+ const handleToggle = (event: Event) => onToggle?.((event.target as HTMLInputElement).checked);
34
+
35
+ const wrapper = tv({
36
+ base: 'relative flex flex-col justify-center',
37
+ variants: {
38
+ disabled: {
39
+ true: 'cursor-not-allowed',
40
+ false: 'cursor-pointer',
41
+ },
42
+ },
43
+ });
44
+
45
+ const bar = tv({
46
+ base: 'w-12 h-3 my-2 rounded-full border border-transparent',
47
+ variants: {
48
+ fillColor: {
49
+ default: 'bg-gray-400',
50
+ primary: 'bg-primary/50',
51
+ secondary: 'bg-dark/50',
52
+ success: 'bg-success/50',
53
+ danger: 'bg-danger/50',
54
+ warning: 'bg-warning/50',
55
+ info: 'bg-info/50',
56
+ },
57
+ },
58
+ });
59
+
60
+ const dot = tv({
61
+ base: 'absolute transition-colors h-6 w-6 rounded-full transition-transform duration-[400ms]',
62
+ variants: {
63
+ checked: {
64
+ true: 'translate-x-6',
65
+ false: '',
66
+ },
67
+ fillColor: {
68
+ default: 'bg-gray-600',
69
+ primary: 'bg-primary',
70
+ secondary: 'bg-dark',
71
+ success: 'bg-success',
72
+ danger: 'bg-danger',
73
+ warning: 'bg-warning',
74
+ info: 'bg-info',
75
+ },
76
+ },
77
+ });
78
+ </script>
79
+
80
+ <label class={cleanClass(className)}>
81
+ {label}
82
+ <span class={wrapper({ disabled })}>
83
+ <input
84
+ class="hidden"
85
+ type="checkbox"
86
+ bind:checked
87
+ onclick={handleToggle}
88
+ {required}
89
+ aria-required={required}
90
+ {disabled}
91
+ aria-disabled={disabled}
92
+ readonly={readOnly}
93
+ aria-readonly={readOnly}
94
+ {...restProps}
95
+ />
96
+ <span class={bar({ fillColor: enabled ? color : 'default' })}> </span>
97
+ <span class={dot({ checked: enabled, fillColor: enabled ? color : 'default' })}></span>
98
+ </span>
99
+ </label>
@@ -0,0 +1,10 @@
1
+ import type { Color } from '../../types.js';
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ declare const Switch: import("svelte").Component<{
4
+ checked?: boolean;
5
+ color?: Color;
6
+ disabled?: boolean;
7
+ class?: string;
8
+ onToggle?: ((checked: boolean) => void) | undefined;
9
+ } & HTMLInputAttributes, {}, "checked">;
10
+ export default Switch;
@@ -5,7 +5,7 @@
5
5
  import { tv } from 'tailwind-variants';
6
6
 
7
7
  type Props = {
8
- color?: Color;
8
+ color?: Color | 'muted';
9
9
  class?: string;
10
10
  size?: Size;
11
11
  children: Snippet;
@@ -13,11 +13,19 @@
13
13
  fontWeight?: 'light' | 'normal' | 'semi-bold' | 'bold';
14
14
  };
15
15
 
16
- const { color, size, fontWeight = 'normal', children, class: className }: Props = $props();
16
+ const {
17
+ color,
18
+ size,
19
+ variant,
20
+ fontWeight = 'normal',
21
+ children,
22
+ class: className,
23
+ }: Props = $props();
17
24
 
18
25
  const styles = tv({
19
26
  variants: {
20
27
  color: {
28
+ muted: 'text-gray-600 dark:text-gray-400',
21
29
  primary: 'text-primary',
22
30
  secondary: 'text-dark',
23
31
  success: 'text-success',
@@ -26,6 +34,10 @@
26
34
  info: 'text-info',
27
35
  },
28
36
 
37
+ variant: {
38
+ italic: 'italic',
39
+ },
40
+
29
41
  size: {
30
42
  tiny: 'text-xs',
31
43
  small: 'text-sm',
@@ -44,6 +56,6 @@
44
56
  });
45
57
  </script>
46
58
 
47
- <p class={cleanClass(styles({ color, size, fontWeight }), className)}>
59
+ <p class={cleanClass(styles({ color, size, fontWeight, variant }), className)}>
48
60
  {@render children()}
49
61
  </p>
@@ -1,7 +1,7 @@
1
1
  import type { Color, Size } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  declare const Text: import("svelte").Component<{
4
- color?: Color;
4
+ color?: Color | "muted";
5
5
  class?: string;
6
6
  size?: Size;
7
7
  children: Snippet;
@@ -1,6 +1,9 @@
1
1
  export declare enum ChildKey {
2
2
  Field = "field",
3
3
  HelperText = "helped-text",
4
+ AppShell = "app-shell",
5
+ AppShellHeader = "app-shell-header",
6
+ AppShellSidebar = "app-shell-sidebar",
4
7
  Card = "card",
5
8
  CardHeader = "card-header",
6
9
  CardBody = "card-body",
package/dist/constants.js CHANGED
@@ -2,6 +2,9 @@ export var ChildKey;
2
2
  (function (ChildKey) {
3
3
  ChildKey["Field"] = "field";
4
4
  ChildKey["HelperText"] = "helped-text";
5
+ ChildKey["AppShell"] = "app-shell";
6
+ ChildKey["AppShellHeader"] = "app-shell-header";
7
+ ChildKey["AppShellSidebar"] = "app-shell-sidebar";
5
8
  ChildKey["Card"] = "card";
6
9
  ChildKey["CardHeader"] = "card-header";
7
10
  ChildKey["CardBody"] = "card-body";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,17 @@
1
+ export { default as appStoreBadge } from './assets/appstore-badge.svg';
2
+ export { default as fdroidBadge } from './assets/fdroid-badge.svg';
3
+ export { default as immichLogoInlineDark } from './assets/immich-logo-inline-dark.svg';
4
+ export { default as immichLogoInlineLight } from './assets/immich-logo-inline-light.svg';
5
+ export { default as immichLogoStackedDark } from './assets/immich-logo-stacked-dark.svg';
6
+ export { default as immichLogoStackedLight } from './assets/immich-logo-stacked-light.svg';
7
+ export { default as immichLogoJson } from './assets/immich-logo.json';
8
+ export { default as immichLogo } from './assets/immich-logo.svg';
9
+ export { default as playStoreBadge } from './assets/playstore-badge.png';
1
10
  export { default as Alert } from './components/Alert/Alert.svelte';
11
+ export { default as AppShell } from './components/AppShell/AppShell.svelte';
12
+ export { default as AppShellHeader } from './components/AppShell/AppShellHeader.svelte';
13
+ export { default as AppShellSidebar } from './components/AppShell/AppShellSidebar.svelte';
14
+ export { default as Avatar } from './components/Avatar/Avatar.svelte';
2
15
  export { default as Button } from './components/Button/Button.svelte';
3
16
  export { default as Card } from './components/Card/Card.svelte';
4
17
  export { default as CardBody } from './components/Card/CardBody.svelte';
@@ -7,19 +20,31 @@ export { default as CardFooter } from './components/Card/CardFooter.svelte';
7
20
  export { default as CardHeader } from './components/Card/CardHeader.svelte';
8
21
  export { default as CardTitle } from './components/Card/CardTitle.svelte';
9
22
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
23
+ export { default as Code } from './components/Code/Code.svelte';
10
24
  export { default as Checkbox } from './components/Form/Checkbox.svelte';
11
25
  export { default as Field } from './components/Form/Field.svelte';
12
26
  export { default as HelperText } from './components/Form/HelperText.svelte';
13
27
  export { default as Input } from './components/Form/Input.svelte';
14
28
  export { default as Label } from './components/Form/Label.svelte';
29
+ export { default as PasswordInput } from './components/Form/PasswordInput.svelte';
30
+ export { default as FormatBytes } from './components/FormatBytes/FormatBytes.svelte';
15
31
  export { default as Heading } from './components/Heading/Heading.svelte';
16
32
  export { default as Icon } from './components/Icon/Icon.svelte';
17
33
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
18
34
  export { default as Link } from './components/Link/Link.svelte';
35
+ export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
19
36
  export { default as Logo } from './components/Logo/Logo.svelte';
37
+ export { default as MultiSelect } from './components/MultiSelect/MultiSelect.svelte';
38
+ export { default as NavbarGroup } from './components/Navbar/NavbarGroup.svelte';
39
+ export { default as NavbarItem } from './components/Navbar/NavbarItem.svelte';
40
+ export { default as Scrollable } from './components/Scrollable/Scrollable.svelte';
41
+ export { default as Select } from './components/Select/Select.svelte';
20
42
  export { default as HStack } from './components/Stack/HStack.svelte';
21
43
  export { default as Stack } from './components/Stack/Stack.svelte';
22
44
  export { default as VStack } from './components/Stack/VStack.svelte';
23
45
  export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
46
+ export { default as Switch } from './components/Switch/Switch.svelte';
24
47
  export { default as Text } from './components/Text/Text.svelte';
48
+ export * from './services/theme.svelte.js';
25
49
  export * from './types.js';
50
+ export * from './utilities/byte-units.js';
package/dist/index.js CHANGED
@@ -1,4 +1,19 @@
1
+ // files
2
+ export { default as appStoreBadge } from './assets/appstore-badge.svg';
3
+ export { default as fdroidBadge } from './assets/fdroid-badge.svg';
4
+ export { default as immichLogoInlineDark } from './assets/immich-logo-inline-dark.svg';
5
+ export { default as immichLogoInlineLight } from './assets/immich-logo-inline-light.svg';
6
+ export { default as immichLogoStackedDark } from './assets/immich-logo-stacked-dark.svg';
7
+ export { default as immichLogoStackedLight } from './assets/immich-logo-stacked-light.svg';
8
+ export { default as immichLogoJson } from './assets/immich-logo.json';
9
+ export { default as immichLogo } from './assets/immich-logo.svg';
10
+ export { default as playStoreBadge } from './assets/playstore-badge.png';
11
+ // components
1
12
  export { default as Alert } from './components/Alert/Alert.svelte';
13
+ export { default as AppShell } from './components/AppShell/AppShell.svelte';
14
+ export { default as AppShellHeader } from './components/AppShell/AppShellHeader.svelte';
15
+ export { default as AppShellSidebar } from './components/AppShell/AppShellSidebar.svelte';
16
+ export { default as Avatar } from './components/Avatar/Avatar.svelte';
2
17
  export { default as Button } from './components/Button/Button.svelte';
3
18
  export { default as Card } from './components/Card/Card.svelte';
4
19
  export { default as CardBody } from './components/Card/CardBody.svelte';
@@ -7,19 +22,32 @@ export { default as CardFooter } from './components/Card/CardFooter.svelte';
7
22
  export { default as CardHeader } from './components/Card/CardHeader.svelte';
8
23
  export { default as CardTitle } from './components/Card/CardTitle.svelte';
9
24
  export { default as CloseButton } from './components/CloseButton/CloseButton.svelte';
25
+ export { default as Code } from './components/Code/Code.svelte';
10
26
  export { default as Checkbox } from './components/Form/Checkbox.svelte';
11
27
  export { default as Field } from './components/Form/Field.svelte';
12
28
  export { default as HelperText } from './components/Form/HelperText.svelte';
13
29
  export { default as Input } from './components/Form/Input.svelte';
14
30
  export { default as Label } from './components/Form/Label.svelte';
31
+ export { default as PasswordInput } from './components/Form/PasswordInput.svelte';
32
+ export { default as FormatBytes } from './components/FormatBytes/FormatBytes.svelte';
15
33
  export { default as Heading } from './components/Heading/Heading.svelte';
16
34
  export { default as Icon } from './components/Icon/Icon.svelte';
17
35
  export { default as IconButton } from './components/IconButton/IconButton.svelte';
18
36
  export { default as Link } from './components/Link/Link.svelte';
37
+ export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner.svelte';
19
38
  export { default as Logo } from './components/Logo/Logo.svelte';
39
+ export { default as MultiSelect } from './components/MultiSelect/MultiSelect.svelte';
40
+ export { default as NavbarGroup } from './components/Navbar/NavbarGroup.svelte';
41
+ export { default as NavbarItem } from './components/Navbar/NavbarItem.svelte';
42
+ export { default as Scrollable } from './components/Scrollable/Scrollable.svelte';
43
+ export { default as Select } from './components/Select/Select.svelte';
20
44
  export { default as HStack } from './components/Stack/HStack.svelte';
21
45
  export { default as Stack } from './components/Stack/Stack.svelte';
22
46
  export { default as VStack } from './components/Stack/VStack.svelte';
23
47
  export { default as SupporterBadge } from './components/SupporterBadge/SupporterBadge.svelte';
48
+ export { default as Switch } from './components/Switch/Switch.svelte';
24
49
  export { default as Text } from './components/Text/Text.svelte';
50
+ // helpers
51
+ export * from './services/theme.svelte.js';
25
52
  export * from './types.js';
53
+ export * from './utilities/byte-units.js';
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import type { ButtonProps } from '../types.js';
2
+ import LoadingSpinner from '../components/LoadingSpinner/LoadingSpinner.svelte';
3
+ import type { ButtonProps, Size } from '../types.js';
3
4
  import { cleanClass } from '../utils.js';
4
5
  import { Button as ButtonPrimitive } from 'bits-ui';
5
6
  import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
@@ -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',
@@ -80,14 +84,6 @@
80
84
  warning: 'bg-warning/10 text-warning border border-warning hover:bg-warning/20',
81
85
  info: 'bg-info/10 text-info border border-info hover:bg-info/20',
82
86
  },
83
- heroColor: {
84
- primary: 'bg-gradient-to-tr from-primary to-primary/60 text-light hover:bg-primary',
85
- secondary: 'bg-gradient-to-tr from-dark to-dark/60 text-light hover:bg-dark',
86
- success: 'bg-gradient-to-tr from-success to-success/60 text-light hover:bg-success',
87
- danger: 'bg-gradient-to-tr from-danger to-danger/60 text-light hover:bg-danger',
88
- warning: 'bg-gradient-to-tr from-warning to-warning/60 text-light hover:bg-warning',
89
- info: 'bg-gradient-to-tr from-info to-info/60 text-light hover:bg-info',
90
- },
91
87
  ghostColor: {
92
88
  primary: 'text-primary hover:bg-primary/10',
93
89
  secondary: 'text-dark hover:bg-dark/10',
@@ -99,6 +95,14 @@
99
95
  },
100
96
  });
101
97
 
98
+ const spinnerSizes: Record<Size, Size> = {
99
+ tiny: 'tiny',
100
+ small: 'tiny',
101
+ medium: 'small',
102
+ large: 'medium',
103
+ giant: 'large',
104
+ };
105
+
102
106
  const classList = $derived(
103
107
  cleanClass(
104
108
  twMerge(
@@ -111,7 +115,6 @@
111
115
  roundedSize: shape === 'semi-round' ? size : undefined,
112
116
  filledColor: variant === 'filled' ? color : undefined,
113
117
  outlineColor: variant === 'outline' ? color : undefined,
114
- heroColor: variant === 'hero' ? color : undefined,
115
118
  ghostColor: variant === 'ghost' ? color : undefined,
116
119
  }),
117
120
  className,
@@ -129,7 +132,16 @@
129
132
  class={classList}
130
133
  type={type as HTMLButtonAttributes['type']}
131
134
  {...restProps as HTMLButtonAttributes}
135
+ {disabled}
136
+ aria-disabled={disabled}
132
137
  >
133
- {@render children?.()}
138
+ {#if loading}
139
+ <div class="flex items-center justify-center gap-2">
140
+ <LoadingSpinner {color} size={spinnerSizes[size]} />
141
+ {@render children?.()}
142
+ </div>
143
+ {:else}
144
+ {@render children?.()}
145
+ {/if}
134
146
  </ButtonPrimitive.Root>
135
147
  {/if}
@@ -0,0 +1,174 @@
1
+ <script lang="ts">
2
+ import { getFieldContext } from '../common/context.svelte.js';
3
+ import Field from '../components/Form/Field.svelte';
4
+ import Input from '../components/Form/Input.svelte';
5
+ import Icon from '../components/Icon/Icon.svelte';
6
+ import IconButton from '../components/IconButton/IconButton.svelte';
7
+ import type { SelectCommonProps, SelectItem } from '../types.js';
8
+ import { cleanClass, generateId } from '../utils.js';
9
+ import { mdiArrowDown, mdiArrowUp, mdiCheck, mdiUnfoldMoreHorizontal } from '@mdi/js';
10
+ import { Select } from 'bits-ui';
11
+ import { tv } from 'tailwind-variants';
12
+
13
+ type T = SelectItem;
14
+
15
+ type Props = {
16
+ multiple?: boolean;
17
+ values?: T[];
18
+ asLabel?: (items: T[]) => string;
19
+ onChange?: (values: T[]) => void;
20
+ } & SelectCommonProps<T>;
21
+
22
+ let {
23
+ data,
24
+ shape,
25
+ color = 'primary',
26
+ size = 'medium',
27
+ multiple = false,
28
+ values = $bindable([]),
29
+ onChange,
30
+ asLabel = (options: T[]) => options.map(({ label }) => label).join(', '),
31
+ placeholder,
32
+ class: className,
33
+ }: Props = $props();
34
+
35
+ const asOptions = (items: string[] | T[]) => {
36
+ return items.map((item) => {
37
+ if (typeof item === 'string') {
38
+ return { value: item, label: item } as T;
39
+ }
40
+
41
+ const label = item.label ?? item.value;
42
+ return { ...item, label };
43
+ });
44
+ };
45
+
46
+ const options = $derived(asOptions(data));
47
+
48
+ const {
49
+ label,
50
+ readOnly = false,
51
+ required = false,
52
+ invalid = false,
53
+ disabled = false,
54
+ } = $derived(getFieldContext());
55
+
56
+ const labelStyles = tv({
57
+ base: '',
58
+ variants: {
59
+ size: {
60
+ tiny: 'text-xs',
61
+ small: 'text-sm',
62
+ medium: 'text-md',
63
+ large: 'text-lg',
64
+ giant: 'text-xl',
65
+ },
66
+ },
67
+ });
68
+
69
+ const id = generateId();
70
+ const inputId = `input-${id}`;
71
+ const labelId = `label-${id}`;
72
+
73
+ const selectedLabel = $derived(asLabel(values));
74
+
75
+ let inputRef = $state<HTMLElement | null>(null);
76
+ let contentRef = $state<HTMLElement | null>(null);
77
+ let ref = $state<HTMLElement | null>(null);
78
+
79
+ $effect(() => {
80
+ if (ref && contentRef) {
81
+ contentRef.style.width = `${ref.clientWidth}px`;
82
+ }
83
+ });
84
+
85
+ const findOption = (value: string) => options.find((option) => option.value === value);
86
+
87
+ const onValueChange = (items: string[] | string) => {
88
+ values = multiple
89
+ ? ((items as string[]).map(findOption) as T[])
90
+ : [findOption(items as string) as T].filter(Boolean);
91
+
92
+ onChange?.(values);
93
+ };
94
+ </script>
95
+
96
+ <div class={cleanClass('flex flex-col gap-1', className)} bind:this={ref}>
97
+ {#if label}
98
+ <label id={labelId} for={inputId} class={labelStyles({ size })}>{label}</label>
99
+ {/if}
100
+
101
+ <Select.Root type={multiple ? 'multiple' : 'single'} {onValueChange}>
102
+ <Select.Trigger
103
+ {disabled}
104
+ class="w-full items-center gap-1 rounded-lg focus-visible:outline-none"
105
+ aria-label={placeholder}
106
+ >
107
+ <Field {readOnly} {required} {disabled} {invalid}>
108
+ <Input
109
+ bind:containerRef={inputRef}
110
+ id={inputId}
111
+ {size}
112
+ {shape}
113
+ {color}
114
+ {placeholder}
115
+ value={selectedLabel}
116
+ readonly
117
+ aria-labelledby={labelId}
118
+ aria-readonly
119
+ >
120
+ {#snippet trailingIcon()}
121
+ <IconButton
122
+ variant="ghost"
123
+ shape="round"
124
+ color="secondary"
125
+ class="m-1"
126
+ icon={mdiUnfoldMoreHorizontal}
127
+ {disabled}
128
+ />
129
+ {/snippet}
130
+ </Input>
131
+ </Field>
132
+ </Select.Trigger>
133
+ <Select.Portal>
134
+ <Select.Content
135
+ bind:ref={contentRef}
136
+ class="max-h-96 select-none rounded-xl border bg-light py-3 text-dark outline-none"
137
+ sideOffset={10}
138
+ >
139
+ <Select.ScrollUpButton class="flex w-full items-center justify-center">
140
+ <Icon icon={mdiArrowUp} />
141
+ </Select.ScrollUpButton>
142
+ <Select.Viewport>
143
+ {#each options as { value, label, disabled }, i (i + value)}
144
+ <Select.Item
145
+ class={cleanClass(
146
+ 'flex h-10 w-full select-none items-center px-5 py-3 text-sm outline-none duration-75 hover:bg-subtle data-[selected]:bg-primary/10 data-[disabled]:opacity-50',
147
+ disabled ? 'cursor-not-allowed' : 'cursor-pointer',
148
+ )}
149
+ {value}
150
+ {label}
151
+ {disabled}
152
+ >
153
+ {#snippet children({ selected })}
154
+ <div
155
+ class="flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors"
156
+ >
157
+ <span>{label}</span>
158
+ </div>
159
+ {#if selected}
160
+ <div class="ml-auto">
161
+ <Icon icon={mdiCheck} />
162
+ </div>
163
+ {/if}
164
+ {/snippet}
165
+ </Select.Item>
166
+ {/each}
167
+ </Select.Viewport>
168
+ <Select.ScrollDownButton class="flex w-full items-center justify-center">
169
+ <Icon icon={mdiArrowDown} />
170
+ </Select.ScrollDownButton>
171
+ </Select.Content>
172
+ </Select.Portal>
173
+ </Select.Root>
174
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { SelectCommonProps, SelectItem } from '../types.js';
2
+ import { Select } from 'bits-ui';
3
+ declare const Select: import("svelte").Component<{
4
+ multiple?: boolean;
5
+ values?: SelectItem[];
6
+ asLabel?: (items: SelectItem[]) => string;
7
+ onChange?: (values: SelectItem[]) => void;
8
+ } & SelectCommonProps<SelectItem>, {}, "values">;
9
+ export default Select;
@@ -0,0 +1,5 @@
1
+ import { Theme } from '../types.js';
2
+ export declare const theme: {
3
+ value: Theme;
4
+ };
5
+ export declare const syncToDom: () => void;
@@ -0,0 +1,13 @@
1
+ import { Theme } from '../types.js';
2
+ export const theme = $state({ value: Theme.Dark });
3
+ export const syncToDom = () => {
4
+ switch (theme.value) {
5
+ case Theme.Dark: {
6
+ document.body.classList.add('dark');
7
+ break;
8
+ }
9
+ default: {
10
+ document.body.classList.remove('dark');
11
+ }
12
+ }
13
+ };