@immich/ui 0.16.0 → 0.17.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.
@@ -27,7 +27,7 @@
27
27
  {#if sidebar}
28
28
  {@render sidebar?.snippet()}
29
29
  {/if}
30
- <Scrollable class="grow">
30
+ <Scrollable class="grow" resetOnNavigate>
31
31
  {@render children?.()}
32
32
  </Scrollable>
33
33
  </div>
@@ -1,17 +1,21 @@
1
1
  <script lang="ts">
2
2
  import Heading from '../Heading/Heading.svelte';
3
- import type { HeadingSize } from '../../types.js';
3
+ import type { HeadingSize, HeadingTag } from '../../types.js';
4
4
  import type { Snippet } from 'svelte';
5
5
 
6
6
  type Props = {
7
+ /**
8
+ * The HTML element type.
9
+ */
10
+ tag?: HeadingTag;
7
11
  class?: string;
8
12
  size?: HeadingSize;
9
13
  children: Snippet;
10
14
  };
11
15
 
12
- const { size = 'small', class: className, children }: Props = $props();
16
+ const { size = 'small', tag, class: className, children }: Props = $props();
13
17
  </script>
14
18
 
15
- <Heading {size} class={className}>
19
+ <Heading {tag} {size} class={className}>
16
20
  {@render children?.()}
17
21
  </Heading>
@@ -1,6 +1,10 @@
1
- import type { HeadingSize } from '../../types.js';
1
+ import type { HeadingSize, HeadingTag } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  type Props = {
4
+ /**
5
+ * The HTML element type.
6
+ */
7
+ tag?: HeadingTag;
4
8
  class?: string;
5
9
  size?: HeadingSize;
6
10
  children: Snippet;
@@ -19,7 +19,7 @@
19
19
  $derived(getFieldContext());
20
20
 
21
21
  const inputStyles = tv({
22
- base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-200 aria-readonly:text-dark/50 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:aria-readonly:text-dark/75',
22
+ base: 'w-full bg-gray-200 outline-none disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-400 aria-readonly:text-dark/50 dark:bg-gray-600 dark:disabled:bg-gray-800 dark:disabled:text-gray-200 dark:aria-readonly:text-dark/75',
23
23
  variants: {
24
24
  shape: {
25
25
  rectangle: 'rounded-none',
@@ -1,28 +1,30 @@
1
1
  <script lang="ts">
2
- import type { HeadingColor, HeadingSize } from '../../types.js';
2
+ import type { HeadingColor, HeadingSize, HeadingTag } from '../../types.js';
3
3
  import { cleanClass } from '../../utils.js';
4
4
  import type { Snippet } from 'svelte';
5
5
  import type { HTMLAttributes } from 'svelte/elements';
6
6
  import { tv } from 'tailwind-variants';
7
7
 
8
8
  type Props = {
9
- size: HeadingSize;
9
+ size?: HeadingSize;
10
+ /**
11
+ * The HTML element type.
12
+ */
13
+ tag?: HeadingTag;
10
14
  color?: HeadingColor;
11
15
  class?: string;
12
16
 
13
17
  children: Snippet;
14
18
  } & HTMLAttributes<HTMLHeadingElement>;
15
19
 
16
- const { color, size = 'medium', class: className, children, ...restProps }: Props = $props();
17
-
18
- const sizes = {
19
- title: 'h1',
20
- giant: 'h2',
21
- large: 'h3',
22
- medium: 'h4',
23
- small: 'h5',
24
- tiny: 'h6',
25
- };
20
+ const {
21
+ color,
22
+ tag = 'p',
23
+ size = 'medium',
24
+ class: className,
25
+ children,
26
+ ...restProps
27
+ }: Props = $props();
26
28
 
27
29
  const styles = tv({
28
30
  base: 'font-bold leading-none tracking-tight',
@@ -47,10 +49,9 @@
47
49
  },
48
50
  });
49
51
 
50
- const tag = $derived(sizes[size] ?? 'h6');
51
52
  const classList = $derived(cleanClass(styles({ color, size }), className));
52
53
  </script>
53
54
 
54
- <svelte:element this={tag} class={classList} role="heading" {...restProps}>
55
+ <svelte:element this={tag} class={classList} {...restProps}>
55
56
  {@render children()}
56
57
  </svelte:element>
@@ -1,8 +1,12 @@
1
- import type { HeadingColor, HeadingSize } from '../../types.js';
1
+ import type { HeadingColor, HeadingSize, HeadingTag } from '../../types.js';
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  type Props = {
5
- size: HeadingSize;
5
+ size?: HeadingSize;
6
+ /**
7
+ * The HTML element type.
8
+ */
9
+ tag?: HeadingTag;
6
10
  color?: HeadingColor;
7
11
  class?: string;
8
12
  children: Snippet;
@@ -69,7 +69,7 @@
69
69
  <Card class={cleanClass(modalStyles({ size }), className)}>
70
70
  <CardHeader class="border-0 py-2">
71
71
  <div class="flex items-center justify-between">
72
- <CardTitle>{title}</CardTitle>
72
+ <CardTitle tag="h1">{title}</CardTitle>
73
73
  <Dialog.Close>
74
74
  <CloseButton size="large" onclick={() => onChange(false)} />
75
75
  </Dialog.Close>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { afterNavigate } from '$app/navigation';
2
3
  import { cleanClass } from '../../utils.js';
3
4
  import type { Snippet } from 'svelte';
4
5
 
@@ -6,12 +7,20 @@
6
7
  class?: string;
7
8
  children?: Snippet;
8
9
  transition?: TransitionEvent;
10
+ ref?: HTMLDivElement;
11
+ resetOnNavigate?: boolean;
9
12
  };
10
13
 
11
- const { class: className, children }: Props = $props();
14
+ let { resetOnNavigate = false, class: className, children, ref = $bindable() }: Props = $props();
15
+
16
+ afterNavigate(() => {
17
+ if (resetOnNavigate) {
18
+ ref?.scrollTo(0, 0);
19
+ }
20
+ });
12
21
  </script>
13
22
 
14
- <div class={cleanClass('immich-scrollbar h-full w-full overflow-auto', className)}>
23
+ <div bind:this={ref} class={cleanClass('immich-scrollbar h-full w-full overflow-auto', className)}>
15
24
  {@render children?.()}
16
25
  </div>
17
26
 
@@ -3,7 +3,9 @@ type Props = {
3
3
  class?: string;
4
4
  children?: Snippet;
5
5
  transition?: TransitionEvent;
6
+ ref?: HTMLDivElement;
7
+ resetOnNavigate?: boolean;
6
8
  };
7
- declare const Scrollable: import("svelte").Component<Props, {}, "">;
9
+ declare const Scrollable: import("svelte").Component<Props, {}, "ref">;
8
10
  type Scrollable = ReturnType<typeof Scrollable>;
9
11
  export default Scrollable;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import IconButton from '../IconButton/IconButton.svelte';
3
- import { theme } from '../../services/theme.svelte.js';
3
+ import { onThemeChange, theme } from '../../services/theme.svelte.js';
4
4
  import { t } from '../../services/translation.svelte.js';
5
5
  import {
6
6
  Theme,
@@ -33,6 +33,7 @@
33
33
  const handleToggleTheme = () => {
34
34
  theme.value = theme.value === Theme.Dark ? Theme.Light : Theme.Dark;
35
35
  onChange?.(theme.value);
36
+ onThemeChange();
36
37
  };
37
38
 
38
39
  const themeIcon = $derived(theme.value === Theme.Light ? mdiWeatherSunny : mdiWeatherNight);
@@ -0,0 +1,11 @@
1
+ type PreferenceOptions<T> = {
2
+ key: string;
3
+ defaults: T;
4
+ onReadError?: (error: unknown) => void;
5
+ onWriteError?: (error: unknown) => void;
6
+ };
7
+ export declare const preference: <T>(options: PreferenceOptions<T>) => {
8
+ state: T;
9
+ sync: () => void;
10
+ };
11
+ export {};
@@ -0,0 +1,37 @@
1
+ import { browser } from '$app/environment';
2
+ const asKey = (key) => `immich-ui-${key}`;
3
+ const readPreference = (key, defaults) => {
4
+ if (!browser || !window?.localStorage) {
5
+ throw new Error('Local storage is not available');
6
+ }
7
+ const text = window.localStorage.getItem(asKey(key));
8
+ const stored = text ? JSON.parse(text) : {};
9
+ return { ...defaults, ...stored };
10
+ };
11
+ const writePreference = (key, value) => {
12
+ if (!browser || !window.localStorage) {
13
+ throw new Error('Local storage is not available');
14
+ }
15
+ const text = JSON.stringify(value);
16
+ window.localStorage.setItem(asKey(key), text);
17
+ };
18
+ export const preference = (options) => {
19
+ const { key, defaults, onReadError, onWriteError } = options;
20
+ let initialValue = defaults;
21
+ try {
22
+ initialValue = readPreference(key, defaults);
23
+ }
24
+ catch (error) {
25
+ onReadError?.(error);
26
+ }
27
+ const state = $state(initialValue);
28
+ const sync = () => {
29
+ try {
30
+ writePreference(key, state);
31
+ }
32
+ catch (error) {
33
+ onWriteError?.(error);
34
+ }
35
+ };
36
+ return { state, sync };
37
+ };
@@ -1,5 +1,17 @@
1
1
  import { Theme } from '../types.js';
2
- export declare const theme: {
2
+ export type ThemeOptions = {
3
+ lightClass?: string;
4
+ darkClass?: string;
5
+ selector?: string;
6
+ };
7
+ export declare const setThemeOptions: (newOptions: ThemeOptions) => {
8
+ lightClass?: string;
9
+ darkClass?: string;
10
+ selector?: string;
11
+ };
12
+ type ThemePreference = {
3
13
  value: Theme;
4
14
  };
5
- export declare const syncToDom: () => void;
15
+ export declare const theme: ThemePreference;
16
+ export declare const onThemeChange: () => void;
17
+ export {};
@@ -1,13 +1,59 @@
1
+ import { browser } from '$app/environment';
2
+ import { preference } from './preference.svelte.js';
1
3
  import { Theme } from '../types.js';
2
- export const theme = $state({ value: Theme.Dark });
3
- export const syncToDom = () => {
4
+ const defaultOptions = {
5
+ darkClass: 'dark',
6
+ selector: 'body',
7
+ };
8
+ let options = $state(defaultOptions);
9
+ export const setThemeOptions = (newOptions) => (options = { ...defaultOptions, ...newOptions });
10
+ const defaultTheme = { value: Theme.Dark };
11
+ const { state, sync: syncToLocalStorage } = preference({
12
+ key: 'theme',
13
+ defaults: defaultTheme,
14
+ onReadError: (error) => console.log(`Preference read error: ${error}`),
15
+ onWriteError: (error) => console.log(`Preference write error: ${error}`),
16
+ });
17
+ export const theme = state;
18
+ export const onThemeChange = () => {
19
+ syncToDom();
20
+ syncToLocalStorage();
21
+ };
22
+ const syncToDom = () => {
23
+ const { lightClass, darkClass, selector } = options;
24
+ if (!browser || !selector) {
25
+ return;
26
+ }
27
+ const element = document.querySelector(selector);
28
+ if (!element) {
29
+ return;
30
+ }
4
31
  switch (theme.value) {
5
32
  case Theme.Dark: {
6
- document.body.classList.add('dark');
33
+ if (lightClass) {
34
+ element.classList.remove(lightClass);
35
+ }
36
+ if (darkClass) {
37
+ element.classList.add(darkClass);
38
+ }
39
+ const darkReaderLock = document.createElement('meta');
40
+ darkReaderLock.name = 'darkreader-lock';
41
+ document.head.appendChild(darkReaderLock);
7
42
  break;
8
43
  }
9
- default: {
10
- document.body.classList.remove('dark');
44
+ case Theme.Light: {
45
+ if (lightClass) {
46
+ element.classList.add(lightClass);
47
+ }
48
+ if (darkClass) {
49
+ element.classList.remove(darkClass);
50
+ }
51
+ const darkReaderLock = document.querySelector('head > meta[name=darkreader-lock]');
52
+ if (darkReaderLock) {
53
+ document.head.removeChild(darkReaderLock);
54
+ }
55
+ break;
11
56
  }
12
57
  }
13
58
  };
59
+ syncToDom();
package/dist/types.d.ts CHANGED
@@ -8,6 +8,7 @@ export type Size = 'tiny' | 'small' | 'medium' | 'large' | 'giant';
8
8
  export type ModalSize = Size | 'full';
9
9
  export type ContainerSize = ModalSize;
10
10
  export type HeadingSize = Size | 'title';
11
+ export type HeadingTag = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
11
12
  export type Shape = 'rectangle' | 'semi-round' | 'round';
12
13
  export type Variants = 'filled' | 'outline' | 'ghost';
13
14
  export type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@immich/ui",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "license": "GNU Affero General Public License version 3",
5
5
  "scripts": {
6
6
  "create": "node scripts/create.js",
@@ -49,7 +49,7 @@
49
49
  "eslint": "^9.7.0",
50
50
  "eslint-config-prettier": "^10.0.0",
51
51
  "eslint-plugin-svelte": "^2.36.0",
52
- "globals": "^15.0.0",
52
+ "globals": "^16.0.0",
53
53
  "prettier": "^3.3.2",
54
54
  "prettier-plugin-svelte": "^3.2.6",
55
55
  "prettier-plugin-tailwindcss": "^0.6.5",