mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat(web): expand/collapse sidebar (#16768)
* feat: expand/collapse sidebar * fix: general PR cleanup - add skip link unit test - remove unused tailwind styles - adjust asset grid spacing - fix event propogation * fix: cleaning up event listeners * fix: purchase modal and button on small screens * fix: explicit tailwind classes * fix: no animation on initial page load * fix: sidebar spacing and reactivity * chore: reverting changes to icons in nav and account info panel * fix: remove left margin from the asset grid after merging in new timeline * chore: extract search-bar changes for a separate PR * fix: add margin to memories
This commit is contained in:
parent
00d3b8d83a
commit
6e62c09d84
@ -864,6 +864,7 @@
|
||||
"loop_videos": "Loop videos",
|
||||
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
|
||||
"main_branch_warning": "You’re using a development version; we strongly recommend using a release version!",
|
||||
"main_menu": "Main menu",
|
||||
"make": "Make",
|
||||
"manage_shared_links": "Manage shared links",
|
||||
"manage_sharing_with_partners": "Manage sharing with partners",
|
||||
|
@ -3,15 +3,16 @@
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
let { show = $bindable() }: Props = $props();
|
||||
let { show = $bindable(), active = $bindable() }: Props = $props();
|
||||
</script>
|
||||
|
||||
<button type="button" onclick={() => (show = true)}>Open</button>
|
||||
|
||||
{#if show}
|
||||
<div use:focusTrap>
|
||||
<div use:focusTrap={{ active }}>
|
||||
<div>
|
||||
<span>text</span>
|
||||
<button data-testid="one" type="button" onclick={() => (show = false)}>Close</button>
|
||||
|
@ -12,6 +12,12 @@ describe('focusTrap action', () => {
|
||||
expect(document.activeElement).toEqual(screen.getByTestId('one'));
|
||||
});
|
||||
|
||||
it('should not set focus if inactive', async () => {
|
||||
render(FocusTrapTest, { show: true, active: false });
|
||||
await tick();
|
||||
expect(document.activeElement).toBe(document.body);
|
||||
});
|
||||
|
||||
it('supports backward focus wrapping', async () => {
|
||||
render(FocusTrapTest, { show: true });
|
||||
await tick();
|
||||
|
@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClick, true);
|
||||
document.addEventListener('mousedown', handleClick, false);
|
||||
node.addEventListener('keydown', handleKey, false);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('mousedown', handleClick, true);
|
||||
document.removeEventListener('mousedown', handleClick, false);
|
||||
node.removeEventListener('keydown', handleKey, false);
|
||||
},
|
||||
};
|
||||
|
@ -1,16 +1,34 @@
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
const selectors =
|
||||
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
interface Options {
|
||||
/**
|
||||
* Set whether the trap is active or not.
|
||||
*/
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export function focusTrap(container: HTMLElement) {
|
||||
const selectors =
|
||||
'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)';
|
||||
|
||||
export function focusTrap(container: HTMLElement, options?: Options) {
|
||||
const triggerElement = document.activeElement;
|
||||
|
||||
const focusableElement = container.querySelector<HTMLElement>(selectors);
|
||||
const withDefaults = (options?: Options) => {
|
||||
return {
|
||||
active: options?.active ?? true,
|
||||
};
|
||||
};
|
||||
|
||||
// Use tick() to ensure focus trap works correctly inside <Portal />
|
||||
void tick().then(() => focusableElement?.focus());
|
||||
const setInitialFocus = () => {
|
||||
const focusableElement = container.querySelector<HTMLElement>(selectors);
|
||||
// Use tick() to ensure focus trap works correctly inside <Portal />
|
||||
void tick().then(() => focusableElement?.focus());
|
||||
};
|
||||
|
||||
if (withDefaults(options).active) {
|
||||
setInitialFocus();
|
||||
}
|
||||
|
||||
const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
|
||||
const focusableElements = container.querySelectorAll<HTMLElement>(selectors);
|
||||
@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) {
|
||||
shortcut: { key: 'Tab' },
|
||||
onShortcut: (event) => {
|
||||
const [firstElement, lastElement] = getFocusableElements();
|
||||
if (document.activeElement === lastElement) {
|
||||
if (document.activeElement === lastElement && withDefaults(options).active) {
|
||||
event.preventDefault();
|
||||
firstElement?.focus();
|
||||
}
|
||||
@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) {
|
||||
shortcut: { key: 'Tab', shift: true },
|
||||
onShortcut: (event) => {
|
||||
const [firstElement, lastElement] = getFocusableElements();
|
||||
if (document.activeElement === firstElement) {
|
||||
if (document.activeElement === firstElement && withDefaults(options).active) {
|
||||
event.preventDefault();
|
||||
lastElement?.focus();
|
||||
}
|
||||
@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) {
|
||||
]);
|
||||
|
||||
return {
|
||||
update(newOptions?: Options) {
|
||||
options = newOptions;
|
||||
if (withDefaults(options).active) {
|
||||
setInitialFocus();
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
destroyShortcuts?.();
|
||||
if (triggerElement instanceof HTMLElement) {
|
||||
|
@ -7,10 +7,17 @@
|
||||
* Target for the skip link to move focus to.
|
||||
*/
|
||||
target?: string;
|
||||
/**
|
||||
* Text for the skip link button.
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* Breakpoint at which the skip link is visible. Defaults to always being visible.
|
||||
*/
|
||||
breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
}
|
||||
|
||||
let { target = 'main', text = $t('skip_to_content') }: Props = $props();
|
||||
let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props();
|
||||
|
||||
let isFocused = $state(false);
|
||||
|
||||
@ -18,6 +25,29 @@
|
||||
const targetEl = document.querySelector<HTMLElement>(target);
|
||||
targetEl?.focus();
|
||||
};
|
||||
|
||||
const getBreakpoint = () => {
|
||||
if (!breakpoint) {
|
||||
return '';
|
||||
}
|
||||
switch (breakpoint) {
|
||||
case 'sm': {
|
||||
return 'hidden sm:block';
|
||||
}
|
||||
case 'md': {
|
||||
return 'hidden md:block';
|
||||
}
|
||||
case 'lg': {
|
||||
return 'hidden lg:block';
|
||||
}
|
||||
case 'xl': {
|
||||
return 'hidden xl:block';
|
||||
}
|
||||
case '2xl': {
|
||||
return 'hidden 2xl:block';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
|
||||
@ -25,6 +55,7 @@
|
||||
size="sm"
|
||||
rounded="none"
|
||||
onclick={moveFocus}
|
||||
class={getBreakpoint()}
|
||||
onfocus={() => (isFocused = true)}
|
||||
onblur={() => (isFocused = false)}
|
||||
>
|
||||
|
@ -51,7 +51,7 @@
|
||||
</header>
|
||||
<main
|
||||
tabindex="-1"
|
||||
class="relative grid h-screen grid-cols-[theme(spacing.18)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
||||
class="relative grid h-screen grid-cols-[theme(spacing.0)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
||||
>
|
||||
{#if sidebar}{@render sidebar()}{:else if admin}
|
||||
<AdminSideBar />
|
||||
@ -66,7 +66,7 @@
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if title}
|
||||
<div class="font-medium" tabindex="-1" id={headerId}>{title}</div>
|
||||
<div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div>
|
||||
{/if}
|
||||
{#if description}
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
|
||||
|
@ -726,7 +726,7 @@
|
||||
class={[
|
||||
'scrollbar-hidden h-full overflow-y-auto outline-none',
|
||||
{ 'm-0': isEmpty },
|
||||
{ 'ml-4 tall:ml-0': !isEmpty },
|
||||
{ 'ml-0': !isEmpty },
|
||||
{ 'mr-[60px]': !isEmpty && !usingMobileDevice },
|
||||
]}
|
||||
tabindex="-1"
|
||||
|
@ -38,7 +38,7 @@
|
||||
<section
|
||||
id="memory-lane"
|
||||
bind:this={memoryLaneElement}
|
||||
class="relative mt-5 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
|
||||
class="relative mt-5 mx-2 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
|
||||
style="scrollbar-width:none"
|
||||
use:resizeObserver={({ width }) => (offsetWidth = width)}
|
||||
onscroll={onScroll}
|
||||
|
@ -1,3 +1,7 @@
|
||||
<script lang="ts" module>
|
||||
export const menuButtonId = 'top-menu-button';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
@ -12,13 +16,14 @@
|
||||
import { handleLogout } from '$lib/utils/auth';
|
||||
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ThemeButton from '../theme-button.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
import AccountInfoPanel from './account-info-panel.svelte';
|
||||
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
|
||||
interface Props {
|
||||
@ -57,11 +62,34 @@
|
||||
>
|
||||
<SkipLink text={$t('skip_to_content')} />
|
||||
<div
|
||||
class="grid h-full grid-cols-[theme(spacing.18)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
||||
class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
||||
>
|
||||
<a data-sveltekit-preload-data="hover" class="ml-4" href={AppRoute.PHOTOS}>
|
||||
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
|
||||
</a>
|
||||
<div class="flex flex-row gap-1 mx-4 items-center">
|
||||
<div>
|
||||
<IconButton
|
||||
id={menuButtonId}
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="large"
|
||||
aria-label={$t('main_menu')}
|
||||
icon={mdiMenu}
|
||||
onclick={() => {
|
||||
isSidebarOpen.value = !isSidebarOpen.value;
|
||||
}}
|
||||
onmousedown={(event: MouseEvent) => {
|
||||
if (isSidebarOpen.value) {
|
||||
// stops event from reaching the default handler when clicking outside of the sidebar
|
||||
event.stopPropagation();
|
||||
}
|
||||
}}
|
||||
class="md:hidden"
|
||||
/>
|
||||
</div>
|
||||
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
|
||||
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
|
||||
<div class="hidden w-full max-w-5xl flex-1 tall:pl-0 sm:block">
|
||||
{#if $featureFlags.search}
|
||||
@ -80,7 +108,6 @@
|
||||
href={AppRoute.SEARCH}
|
||||
id="search-button"
|
||||
class="sm:hidden"
|
||||
title={$t('go_to_search')}
|
||||
aria-label={$t('go_to_search')}
|
||||
/>
|
||||
{/if}
|
||||
@ -120,7 +147,6 @@
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
title={$t('support_and_feedback')}
|
||||
icon={mdiHelpCircleOutline}
|
||||
onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
|
||||
aria-label={$t('support_and_feedback')}
|
||||
|
@ -8,7 +8,9 @@
|
||||
</script>
|
||||
|
||||
<!-- Individual Purchase Option -->
|
||||
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
|
||||
<div
|
||||
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
|
||||
>
|
||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiAccount} size="56" />
|
||||
<p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>
|
||||
|
@ -57,7 +57,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-6 mt-4 justify-between">
|
||||
<div class="flex flex-col sm:flex-row gap-6 mt-4 justify-between">
|
||||
<ServerPurchaseOptionCard />
|
||||
<UserPurchaseOptionCard />
|
||||
</div>
|
||||
|
@ -8,7 +8,9 @@
|
||||
</script>
|
||||
|
||||
<!-- SERVER Purchase Options -->
|
||||
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
|
||||
<div
|
||||
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
|
||||
>
|
||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiServer} size="56" />
|
||||
<p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>
|
||||
|
@ -78,7 +78,7 @@
|
||||
<LicenseModal onClose={() => (isOpen = false)} />
|
||||
{/if}
|
||||
|
||||
<div class="hidden md:block license-status pl-4 text-sm">
|
||||
<div class="license-status pl-4 text-sm">
|
||||
{#if $isPurchased && $preferences.purchase.showSupportBadge}
|
||||
<button
|
||||
onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
|
||||
@ -95,7 +95,7 @@
|
||||
onmouseleave={() => (hoverButton = false)}
|
||||
onfocus={onButtonHover}
|
||||
onblur={() => (hoverButton = false)}
|
||||
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 w-full"
|
||||
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 min-w-52 w-full"
|
||||
>
|
||||
<div class="flex justify-between w-full place-items-center place-content-center">
|
||||
<div class="flex place-items-center place-content-center gap-1">
|
||||
@ -110,7 +110,7 @@
|
||||
<div>
|
||||
<Icon
|
||||
path={mdiInformationOutline}
|
||||
class="flex text-immich-primary dark:text-immich-dark-primary font-medium"
|
||||
class="hidden md:flex text-immich-primary dark:text-immich-dark-primary font-medium"
|
||||
size="18"
|
||||
/>
|
||||
</div>
|
||||
@ -123,7 +123,7 @@
|
||||
{#if showMessage}
|
||||
<dialog
|
||||
open
|
||||
class="w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
||||
class="hidden md:block w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
||||
transition:fade={{ duration: 150 }}
|
||||
onmouseover={() => (hoverMessage = true)}
|
||||
onmouseleave={() => (hoverMessage = false)}
|
||||
|
@ -42,7 +42,7 @@
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
|
||||
class="text-sm flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between min-w-52 overflow-hidden"
|
||||
>
|
||||
{#if $connected}
|
||||
<div class="flex gap-2 place-items-center place-content-center">
|
||||
|
@ -62,11 +62,9 @@
|
||||
class="flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray 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 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
|
||||
: ''}
|
||||
pl-5 group-hover:sm:px-5 md:px-5
|
||||
"
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
|
||||
<div class="flex w-full place-items-center gap-4 pl-5 overflow-hidden truncate">
|
||||
<Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
|
||||
<span class="text-sm font-medium">{title}</span>
|
||||
</div>
|
||||
|
@ -1,17 +1,56 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import { menuButtonId } from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
const mdBreakpoint = 768;
|
||||
|
||||
let { children }: Props = $props();
|
||||
|
||||
let innerWidth: number = $state(0);
|
||||
|
||||
const closeSidebar = (width: number) => {
|
||||
isSidebarOpen.value = width >= mdBreakpoint;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
closeSidebar(innerWidth);
|
||||
});
|
||||
|
||||
const isHidden = $derived(!isSidebarOpen.value && innerWidth < mdBreakpoint);
|
||||
const isExpanded = $derived(isSidebarOpen.value && innerWidth < mdBreakpoint);
|
||||
|
||||
const handleClickOutside = () => {
|
||||
if (!isSidebarOpen.value) {
|
||||
return;
|
||||
}
|
||||
closeSidebar(innerWidth);
|
||||
if (isHidden) {
|
||||
document.querySelector<HTMLButtonElement>(`#${menuButtonId}`)?.focus();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth />
|
||||
<section
|
||||
id="sidebar"
|
||||
tabindex="-1"
|
||||
class="immich-scrollbar group relative z-10 flex w-18 flex-col gap-1 overflow-y-auto bg-immich-bg pt-8 max-md:pt-16 transition-all duration-200 dark:bg-immich-dark-bg hover:sm:w-64 hover:sm:border-r hover:sm:pr-6 hover:sm:shadow-2xl hover:sm:dark:border-r-immich-dark-gray md:w-64 md:pr-6 hover:md:border-none hover:md:shadow-none"
|
||||
class="immich-scrollbar relative z-10 w-0 md:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
|
||||
class:shadow-2xl={isExpanded}
|
||||
class:dark:border-r-immich-dark-gray={isExpanded}
|
||||
class:border-r={isExpanded}
|
||||
class:w-[min(100vw,16rem)]={isSidebarOpen.value}
|
||||
inert={isHidden}
|
||||
use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}
|
||||
use:focusTrap={{ active: isExpanded }}
|
||||
>
|
||||
{@render children?.()}
|
||||
<div class="pr-6 flex flex-col gap-1 h-max min-h-full">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
|
@ -84,10 +84,7 @@
|
||||
bind:isSelected={isSharingSelected}
|
||||
></SideBarLink>
|
||||
|
||||
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
|
||||
<p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
|
||||
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
|
||||
</div>
|
||||
<p class="text-xs p-6 dark:text-immich-dark-fg">{$t('library').toUpperCase()}</p>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('favorites')}
|
||||
|
@ -46,7 +46,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
|
||||
class="storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm min-w-52"
|
||||
title={$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale, 3),
|
||||
@ -54,26 +54,24 @@
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div class="hidden group-hover:sm:block md:block">
|
||||
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
|
||||
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
|
||||
|
||||
{#if userInteraction.serverInfo}
|
||||
<p class="text-gray-500 dark:text-gray-300">
|
||||
{$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale),
|
||||
available: getByteUnitString(availableBytes, $locale),
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
{#if userInteraction.serverInfo}
|
||||
<p class="text-gray-500 dark:text-gray-300">
|
||||
{$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale),
|
||||
available: getByteUnitString(availableBytes, $locale),
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%"></div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -32,6 +32,7 @@
|
||||
href={getLink(path)}
|
||||
title={value}
|
||||
class={`flex flex-grow place-items-center pl-2 py-1 text-sm rounded-lg hover:bg-slate-200 dark:hover:bg-slate-800 hover:font-semibold ${isTarget ? 'bg-slate-100 dark:bg-slate-700 font-semibold text-immich-primary dark:text-immich-dark-primary' : 'dark:text-gray-200'}`}
|
||||
data-sveltekit-keepfocus
|
||||
>
|
||||
<button type="button" {onclick} class={Object.values(tree).length === 0 ? 'invisible' : ''}>
|
||||
<Icon path={isOpen ? mdiChevronDown : mdiChevronRight} class="text-gray-400" size={20} />
|
||||
|
1
web/src/lib/stores/side-bar.svelte.ts
Normal file
1
web/src/lib/stores/side-bar.svelte.ts
Normal file
@ -0,0 +1 @@
|
||||
export const isSidebarOpen = $state({ value: false });
|
@ -131,7 +131,7 @@
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#snippet sidebar()}
|
||||
<SideBarSection>
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} />
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" />
|
||||
<section>
|
||||
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
|
||||
<div class="h-full">
|
||||
|
@ -148,7 +148,7 @@
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#snippet sidebar()}
|
||||
<SideBarSection>
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} />
|
||||
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
|
||||
<section>
|
||||
<div class="text-xs pl-4 mb-2 dark:text-white">{$t('explorer').toUpperCase()}</div>
|
||||
<div class="h-full">
|
||||
|
Loading…
x
Reference in New Issue
Block a user