refactor(web): user sidebar (#25292)

This commit is contained in:
Jason Rasmussen 2026-01-16 11:17:35 -05:00 committed by GitHub
parent fdff591a11
commit a2b03f7650
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 40 additions and 158 deletions

View file

@ -741,8 +741,8 @@ importers:
specifier: file:../open-api/typescript-sdk specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk version: link:../open-api/typescript-sdk
'@immich/ui': '@immich/ui':
specifier: ^0.58.4 specifier: ^0.59.0
version: 0.58.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4) version: 0.59.0(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)
'@mapbox/mapbox-gl-rtl-text': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3 specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3) version: 0.2.3(mapbox-gl@1.13.3)
@ -3128,8 +3128,8 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
'@immich/ui@0.58.4': '@immich/ui@0.59.0':
resolution: {integrity: sha512-/Y+TRA9E8VQ+yH0aqrkEnQTQi4j02dNgahil9NbJe3hSnakfDHZUgJR5xevGZbKqlnBV4O3mjbwmzr6j9wlP7w==} resolution: {integrity: sha512-7yxvyhhd99T0AHhjMakp7c/U4n0jGAmRO5xpncsRASRvqZve/LAibjr6N5FJc5IAd222DROTMLn6imsxVfqfvg==}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
@ -15604,7 +15604,7 @@ snapshots:
dependencies: dependencies:
svelte: 5.46.4 svelte: 5.46.4
'@immich/ui@0.58.4(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)': '@immich/ui@0.59.0(@sveltejs/kit@2.49.5(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.3(svelte@5.46.4)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(svelte@5.46.4)':
dependencies: dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.4) '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.4)
'@internationalized/date': 3.10.0 '@internationalized/date': 3.10.0

View file

@ -27,7 +27,7 @@
"@formatjs/icu-messageformat-parser": "^3.0.0", "@formatjs/icu-messageformat-parser": "^3.0.0",
"@immich/justified-layout-wasm": "^0.4.3", "@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.58.4", "@immich/ui": "^0.59.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0", "@photo-sphere-viewer/core": "^5.14.0",

View file

@ -1,76 +0,0 @@
<script lang="ts">
import { page } from '$app/state';
import { Icon } from '@immich/ui';
import { mdiChevronDown, mdiChevronLeft } from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
title: string;
href: string;
icon: string;
flippedLogo?: boolean;
isSelected?: boolean;
preloadData?: boolean;
dropDownContent?: Snippet;
dropdownOpen?: boolean;
}
let {
title,
href,
icon,
flippedLogo = false,
isSelected = $bindable(false),
preloadData = true,
dropDownContent: hasDropdown,
dropdownOpen = $bindable(false),
}: Props = $props();
$effect(() => {
isSelected = page.url.pathname.startsWith(href);
});
</script>
<div class="relative">
{#if hasDropdown}
<span class="hidden md:block absolute start-1 h-full">
<button
type="button"
aria-label={$t('recent-albums')}
class="relative flex cursor-default pt-4 pb-4 select-none justify-center hover:cursor-pointer hover:bg-subtle hover:fill-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary rounded h-fill"
onclick={() => (dropdownOpen = !dropdownOpen)}
>
<Icon
icon={dropdownOpen ? mdiChevronDown : mdiChevronLeft}
size="1em"
class="shrink-0 delay-100 duration-100 "
flipped={flippedLogo}
aria-hidden
/>
</button>
</span>
{/if}
<!-- safari still needs a tabIndex=0 -->
<a
tabindex="0"
{href}
data-sveltekit-preload-data={preloadData ? 'hover' : 'off'}
draggable="false"
aria-current={isSelected ? 'page' : undefined}
class="flex w-full place-items-center gap-4 rounded-e-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-subtle hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
{isSelected
? 'bg-immich-primary/10 dark:text-primary text-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10'
: ''}"
>
<div class="flex w-full place-items-center gap-4 ps-5 overflow-hidden truncate">
<Icon {icon} size="1.5em" class="shrink-0" flipped={flippedLogo} aria-hidden />
<span class="text-sm font-medium">{title}</span>
</div>
<div></div>
</a>
</div>
{#if hasDropdown && dropdownOpen}
{@render hasDropdown?.()}
{/if}

View file

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths';
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte'; import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte'; import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import { AppRoute } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { recentAlbumsDropdown } from '$lib/stores/preferences.store'; import { recentAlbumsDropdown } from '$lib/stores/preferences.store';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { NavbarGroup, NavbarItem } from '@immich/ui';
import { import {
mdiAccount, mdiAccount,
mdiAccountMultiple, mdiAccountMultiple,
@ -33,119 +34,77 @@
} from '@mdi/js'; } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import SideBarLink from './side-bar-link.svelte';
let isArchiveSelected: boolean = $state(false);
let isFavoritesSelected: boolean = $state(false);
let isMapSelected: boolean = $state(false);
let isPeopleSelected: boolean = $state(false);
let isPhotosSelected: boolean = $state(false);
let isSharingSelected: boolean = $state(false);
let isTrashSelected: boolean = $state(false);
let isUtilitiesSelected: boolean = $state(false);
let isLockedFolderSelected: boolean = $state(false);
</script> </script>
<Sidebar ariaLabel={$t('primary')}> <Sidebar ariaLabel={$t('primary')}>
<SideBarLink <NavbarItem
title={$t('photos')} title={$t('photos')}
href={resolve('/(user)/photos')} href={AppRoute.PHOTOS}
bind:isSelected={isPhotosSelected} icon={mdiImageMultipleOutline}
icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline} activeIcon={mdiImageMultiple}
></SideBarLink> />
{#if featureFlagsManager.value.search} {#if featureFlagsManager.value.search}
<SideBarLink title={$t('explore')} href={resolve('/(user)/explore')} icon={mdiMagnify} /> <NavbarItem title={$t('explore')} href={AppRoute.EXPLORE} icon={mdiMagnify} />
{/if} {/if}
{#if featureFlagsManager.value.map} {#if featureFlagsManager.value.map}
<SideBarLink <NavbarItem title={$t('map')} href={AppRoute.MAP} icon={mdiMapOutline} activeIcon={mdiMap} />
title={$t('map')}
href={resolve('/(user)/map')}
bind:isSelected={isMapSelected}
icon={isMapSelected ? mdiMap : mdiMapOutline}
/>
{/if} {/if}
{#if $preferences.people.enabled && $preferences.people.sidebarWeb} {#if $preferences.people.enabled && $preferences.people.sidebarWeb}
<SideBarLink <NavbarItem title={$t('people')} href={AppRoute.PEOPLE} icon={mdiAccountOutline} activeIcon={mdiAccount} />
title={$t('people')}
href={resolve('/(user)/people')}
bind:isSelected={isPeopleSelected}
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
/>
{/if} {/if}
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb} {#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
<SideBarLink title={$t('shared_links')} href={resolve('/(user)/shared-links')} icon={mdiLink} /> <NavbarItem title={$t('shared_links')} href={AppRoute.SHARED_LINKS} icon={mdiLink} />
{/if} {/if}
<SideBarLink <NavbarItem
title={$t('sharing')} title={$t('sharing')}
href={resolve('/(user)/sharing')} href={AppRoute.SHARING}
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline} icon={mdiAccountMultipleOutline}
bind:isSelected={isSharingSelected} activeIcon={mdiAccountMultiple}
></SideBarLink> />
<p class="text-xs py-5 ps-6 dark:text-immich-dark-fg uppercase">{$t('library')}</p> <NavbarGroup title={$t('library')} size="tiny" />
<SideBarLink <NavbarItem title={$t('favorites')} href={AppRoute.FAVORITES} icon={mdiHeartOutline} activeIcon={mdiHeart} />
title={$t('favorites')}
href={resolve('/(user)/favorites')}
icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
bind:isSelected={isFavoritesSelected}
></SideBarLink>
<SideBarLink <NavbarItem
title={$t('albums')} title={$t('albums')}
href={resolve('/(user)/albums')} href={AppRoute.ALBUMS}
icon={mdiImageAlbum} icon={{ icon: mdiImageAlbum, flipped: true }}
flippedLogo bind:expanded={$recentAlbumsDropdown}
bind:dropdownOpen={$recentAlbumsDropdown}
> >
{#snippet dropDownContent()} {#snippet items()}
<span in:fly={{ y: -20 }} class="hidden md:block"> <span in:fly={{ y: -20 }} class="hidden md:block">
<RecentAlbums /> <RecentAlbums />
</span> </span>
{/snippet} {/snippet}
</SideBarLink> </NavbarItem>
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb} {#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
<SideBarLink title={$t('tags')} href={resolve('/(user)/tags')} icon={mdiTagMultipleOutline} flippedLogo /> <NavbarItem title={$t('tags')} href={AppRoute.TAGS} icon={{ icon: mdiTagMultipleOutline, flipped: true }} />
{/if} {/if}
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb} {#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
<SideBarLink title={$t('folders')} href={resolve('/(user)/folders')} icon={mdiFolderOutline} flippedLogo /> <NavbarItem title={$t('folders')} href={AppRoute.FOLDERS} icon={{ icon: mdiFolderOutline, flipped: true }} />
{/if} {/if}
<SideBarLink <NavbarItem title={$t('utilities')} href={AppRoute.UTILITIES} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
title={$t('utilities')}
href={resolve('/(user)/utilities')}
bind:isSelected={isUtilitiesSelected}
icon={isUtilitiesSelected ? mdiToolbox : mdiToolboxOutline}
></SideBarLink>
<SideBarLink <NavbarItem
title={$t('archive')} title={$t('archive')}
href={resolve('/(user)/archive')} href={AppRoute.ARCHIVE}
bind:isSelected={isArchiveSelected} icon={mdiArchiveArrowDownOutline}
icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline} activeIcon={mdiArchiveArrowDown}
></SideBarLink> />
<SideBarLink <NavbarItem title={$t('locked_folder')} href={AppRoute.LOCKED} icon={mdiLockOutline} activeIcon={mdiLock} />
title={$t('locked_folder')}
href={resolve('/(user)/locked')}
bind:isSelected={isLockedFolderSelected}
icon={isLockedFolderSelected ? mdiLock : mdiLockOutline}
></SideBarLink>
{#if featureFlagsManager.value.trash} {#if featureFlagsManager.value.trash}
<SideBarLink <NavbarItem title={$t('trash')} href={AppRoute.TRASH} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
title={$t('trash')}
href={resolve('/(user)/trash')}
bind:isSelected={isTrashSelected}
icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
></SideBarLink>
{/if} {/if}
<BottomInfo /> <BottomInfo />

View file

@ -30,7 +30,6 @@ export enum AppRoute {
ADMIN_REPAIR = '/admin/repair', ADMIN_REPAIR = '/admin/repair',
ALBUMS = '/albums', ALBUMS = '/albums',
LIBRARIES = '/libraries',
ARCHIVE = '/archive', ARCHIVE = '/archive',
FAVORITES = '/favorites', FAVORITES = '/favorites',
PEOPLE = '/people', PEOPLE = '/people',