diff --git a/i18n/en.json b/i18n/en.json index 8d1cc3a2b3..de17cccebd 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", diff --git a/web/src/lib/actions/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte index e1cb6fa4fb..a19d2b75db 100644 --- a/web/src/lib/actions/__test__/focus-trap-test.svelte +++ b/web/src/lib/actions/__test__/focus-trap-test.svelte @@ -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> diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts index 6ce5ad6d5b..d92d8e037d 100644 --- a/web/src/lib/actions/__test__/focus-trap.spec.ts +++ b/web/src/lib/actions/__test__/focus-trap.spec.ts @@ -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(); diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts index 92775546aa..599a97af75 100644 --- a/web/src/lib/actions/click-outside.ts +++ b/web/src/lib/actions/click-outside.ts @@ -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); }, }; diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts index 7483e76099..1a84f21729 100644 --- a/web/src/lib/actions/focus-trap.ts +++ b/web/src/lib/actions/focus-trap.ts @@ -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) { diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index b80e7d1a44..a1a24634c4 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -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)} > diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 27873f39a5..4944982b60 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -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> diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index d3afdc6072..7f716e70ef 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -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" diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 9536aaf746..ff9264b961 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -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} diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 02b55a1d07..bd4bffd2f6 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -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')} diff --git a/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte index c77a9dac96..815db8de9f 100644 --- a/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte @@ -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> diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte index 6a4e7f1a4b..567fce9281 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -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> diff --git a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte index ffc015233c..19db461229 100644 --- a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte @@ -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> diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index a42e340eae..47e46c59b5 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -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)} diff --git a/web/src/lib/components/shared-components/side-bar/server-status.svelte b/web/src/lib/components/shared-components/side-bar/server-status.svelte index e1d7340c46..8ca552a1f4 100644 --- a/web/src/lib/components/shared-components/side-bar/server-status.svelte +++ b/web/src/lib/components/shared-components/side-bar/server-status.svelte @@ -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"> diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte index 47de0d5db0..f8bf89cd29 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-link.svelte @@ -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> diff --git a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte index 97dbdc7637..4b22a16c9c 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar-section.svelte @@ -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> diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 5493495fd3..ec9c2a06da 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -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')} diff --git a/web/src/lib/components/shared-components/side-bar/storage-space.svelte b/web/src/lib/components/shared-components/side-bar/storage-space.svelte index 9472397565..604522c4f0 100644 --- a/web/src/lib/components/shared-components/side-bar/storage-space.svelte +++ b/web/src/lib/components/shared-components/side-bar/storage-space.svelte @@ -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> diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte index 33f9d14a13..ded6f70690 100644 --- a/web/src/lib/components/shared-components/tree/tree.svelte +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -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} /> diff --git a/web/src/lib/stores/side-bar.svelte.ts b/web/src/lib/stores/side-bar.svelte.ts new file mode 100644 index 0000000000..791ee32c91 --- /dev/null +++ b/web/src/lib/stores/side-bar.svelte.ts @@ -0,0 +1 @@ +export const isSidebarOpen = $state({ value: false }); diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index d0d7bc3359..f5a4f6cd09 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -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"> diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index a89da7ad6b..bf423e4825 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -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">