mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
chore: tailwindcss v4 and z-war clean up (#18358)
* chore: styling tweak * replace full-screen-modal, update docs * scrubber * fix: control app bar in memory viewer * face lift * pr feedback * clean up
This commit is contained in:
parent
2431e04a09
commit
c8641d24f6
42 changed files with 871 additions and 928 deletions
|
|
@ -5,9 +5,9 @@
|
|||
AlbumModalRowType,
|
||||
isSelectableRowType,
|
||||
} from '$lib/components/shared-components/album-selection/album-selection-utils';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import { type AlbumResponseDto, getAllAlbums } from '@immich/sdk';
|
||||
import { Modal, ModalBody } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import AlbumListItem from '../../asset-viewer/album-list-item.svelte';
|
||||
|
|
@ -80,49 +80,51 @@
|
|||
const handleAlbumClick = (album: AlbumResponseDto) => () => onAlbumClick(album);
|
||||
</script>
|
||||
|
||||
<FullScreenModal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose}>
|
||||
<div class="mb-2 flex max-h-[400px] flex-col">
|
||||
{#if loading}
|
||||
<!-- eslint-disable-next-line svelte/require-each-key -->
|
||||
{#each { length: 3 } as _}
|
||||
<div class="flex animate-pulse gap-4 px-6 py-2">
|
||||
<div class="h-12 w-12 rounded-xl bg-slate-200"></div>
|
||||
<div class="flex flex-col items-start justify-center gap-2">
|
||||
<span class="h-4 w-36 animate-pulse bg-slate-200"></span>
|
||||
<div class="flex animate-pulse gap-1">
|
||||
<span class="h-3 w-8 bg-slate-200"></span>
|
||||
<span class="h-3 w-20 bg-slate-200"></span>
|
||||
<Modal title={shared ? $t('add_to_shared_album') : $t('add_to_album')} {onClose} size="small">
|
||||
<ModalBody>
|
||||
<div class="mb-2 flex max-h-[400px] flex-col">
|
||||
{#if loading}
|
||||
<!-- eslint-disable-next-line svelte/require-each-key -->
|
||||
{#each { length: 3 } as _}
|
||||
<div class="flex animate-pulse gap-4 px-6 py-2">
|
||||
<div class="h-12 w-12 rounded-xl bg-slate-200"></div>
|
||||
<div class="flex flex-col items-start justify-center gap-2">
|
||||
<span class="h-4 w-36 animate-pulse bg-slate-200"></span>
|
||||
<div class="flex animate-pulse gap-1">
|
||||
<span class="h-3 w-8 bg-slate-200"></span>
|
||||
<span class="h-3 w-20 bg-slate-200"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<input
|
||||
class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary"
|
||||
placeholder={$t('search')}
|
||||
{onkeydown}
|
||||
bind:value={search}
|
||||
use:initInput
|
||||
/>
|
||||
<div class="immich-scrollbar overflow-y-auto">
|
||||
<!-- eslint-disable-next-line svelte/require-each-key -->
|
||||
{#each albumModalRows as row}
|
||||
{#if row.type === AlbumModalRowType.NEW_ALBUM}
|
||||
<NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} />
|
||||
{:else if row.type === AlbumModalRowType.SECTION}
|
||||
<p class="px-5 py-3 text-xs">{row.text}</p>
|
||||
{:else if row.type === AlbumModalRowType.MESSAGE}
|
||||
<p class="px-5 py-1 text-sm">{row.text}</p>
|
||||
{:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album}
|
||||
<AlbumListItem
|
||||
album={row.album}
|
||||
selected={row.selected || false}
|
||||
searchQuery={search}
|
||||
onAlbumClick={handleAlbumClick(row.album)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
{:else}
|
||||
<input
|
||||
class="border-b-4 border-immich-bg px-6 py-2 text-2xl focus:border-immich-primary dark:border-immich-dark-gray dark:focus:border-immich-dark-primary"
|
||||
placeholder={$t('search')}
|
||||
{onkeydown}
|
||||
bind:value={search}
|
||||
use:initInput
|
||||
/>
|
||||
<div class="immich-scrollbar overflow-y-auto">
|
||||
<!-- eslint-disable-next-line svelte/require-each-key -->
|
||||
{#each albumModalRows as row}
|
||||
{#if row.type === AlbumModalRowType.NEW_ALBUM}
|
||||
<NewAlbumListItem selected={row.selected || false} {onNewAlbum} searchQuery={search} />
|
||||
{:else if row.type === AlbumModalRowType.SECTION}
|
||||
<p class="px-5 py-3 text-xs">{row.text}</p>
|
||||
{:else if row.type === AlbumModalRowType.MESSAGE}
|
||||
<p class="px-5 py-1 text-sm">{row.text}</p>
|
||||
{:else if row.type === AlbumModalRowType.ALBUM_ITEM && row.album}
|
||||
<AlbumListItem
|
||||
album={row.album}
|
||||
selected={row.selected || false}
|
||||
searchQuery={search}
|
||||
onAlbumClick={handleAlbumClick(row.album)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@
|
|||
buttonClass?: string | undefined;
|
||||
hideContent?: boolean;
|
||||
children?: Snippet;
|
||||
offset?: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let {
|
||||
|
|
@ -51,6 +55,7 @@
|
|||
buttonClass = undefined,
|
||||
hideContent = false,
|
||||
children,
|
||||
offset,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
|
||||
|
|
@ -186,13 +191,14 @@
|
|||
]}
|
||||
>
|
||||
<ContextMenu
|
||||
{...contextMenuPosition}
|
||||
{direction}
|
||||
ariaActiveDescendant={$selectedIdStore}
|
||||
ariaLabelledBy={buttonId}
|
||||
bind:menuElement={menuContainer}
|
||||
id={menuId}
|
||||
isVisible={isOpen}
|
||||
x={contextMenuPosition.x - (offset?.x ?? 0)}
|
||||
y={contextMenuPosition.y + (offset?.y ?? 0)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ContextMenu>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
let buttonClass = $derived(forceDark ? 'hover:text-immich-dark-gray' : undefined);
|
||||
</script>
|
||||
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent z-1">
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="absolute top-0 w-full bg-transparent">
|
||||
<nav
|
||||
id="asset-selection-app-bar"
|
||||
class={[
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
appBarBorder,
|
||||
'mx-2 my-2 place-items-center rounded-lg p-2 max-md:p-0 transition-all',
|
||||
tailwindClasses,
|
||||
forceDark ? 'bg-immich-dark-gray text-white' : 'bg-subtle dark:bg-immich-dark-gray',
|
||||
forceDark ? 'bg-immich-dark-gray! text-white' : 'bg-subtle dark:bg-immich-dark-gray',
|
||||
]}
|
||||
>
|
||||
<div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg">
|
||||
|
|
|
|||
|
|
@ -29,5 +29,5 @@
|
|||
{#if title}
|
||||
<h2 class="text-xl font-medium my-4">{title}</h2>
|
||||
{/if}
|
||||
<p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light">{text}</p>
|
||||
<p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light text-center">{text}</p>
|
||||
</svelte:element>
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
/**
|
||||
* If true, the logo will be displayed next to the modal title.
|
||||
*/
|
||||
showLogo?: boolean;
|
||||
/**
|
||||
* Optional icon to display next to the modal title, if `showLogo` is false.
|
||||
*/
|
||||
icon?: string | undefined;
|
||||
/**
|
||||
* Sets the width of the modal.
|
||||
*
|
||||
* - `wide`: 48rem
|
||||
* - `narrow`: 28rem
|
||||
* - `auto`: fits the width of the modal content, up to a maximum of 32rem
|
||||
*/
|
||||
width?: 'extra-wide' | 'wide' | 'narrow' | 'auto';
|
||||
stickyBottom?: Snippet;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
onClose,
|
||||
title,
|
||||
showLogo = false,
|
||||
icon = undefined,
|
||||
width = 'narrow',
|
||||
stickyBottom,
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
/**
|
||||
* Unique identifier for the modal.
|
||||
*/
|
||||
let id: string = generateId();
|
||||
|
||||
let titleId = $derived(`${id}-title`);
|
||||
let isStickyBottom = $derived(!!stickyBottom);
|
||||
|
||||
let modalWidth = $state<string>();
|
||||
|
||||
$effect(() => {
|
||||
switch (width) {
|
||||
case 'extra-wide': {
|
||||
modalWidth = 'w-4xl';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'wide': {
|
||||
modalWidth = 'w-3xl';
|
||||
break;
|
||||
}
|
||||
|
||||
case 'narrow': {
|
||||
modalWidth = 'w-md';
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
modalWidth = 'sm:max-w-4xl';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<section
|
||||
role="presentation"
|
||||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
class="fixed start-0 top-0 flex h-dvh w-dvw place-content-center place-items-center bg-black/40"
|
||||
onkeydown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="flex flex-col max-h-[min(95dvh,60rem)] max-w-[95vw] {modalWidth} overflow-hidden rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg pt-3 pb-4"
|
||||
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
|
||||
tabindex="-1"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
>
|
||||
<div class="immich-scrollbar overflow-y-auto pt-1" class:pb-4={isStickyBottom}>
|
||||
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
|
||||
<div class="px-5 pt-0 mb-5">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{#if isStickyBottom}
|
||||
<div
|
||||
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky pt-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500"
|
||||
>
|
||||
{@render stickyBottom?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<svg {viewBox} class={cssClass}>
|
||||
<title>{$t('immich_logo')}</title>
|
||||
{#if !noText}
|
||||
<g class="st0 dark:fill-[#accbfa]">
|
||||
<g class="st0 dark:fill-[#accbfa] fill-[#4251b0]">
|
||||
<path
|
||||
d="M268.73,63.18c6.34,0,11.52,5.18,11.52,11.35c0,6.34-5.18,11.35-11.52,11.35s-11.69-5.01-11.69-11.35
|
||||
C257.04,68.36,262.39,63.18,268.73,63.18z M258.88,122.45c0-3.01-0.67-7.85-0.67-10.68c0-6.01,4.67-10.68,10.52-10.68
|
||||
|
|
@ -94,9 +94,6 @@
|
|||
</svg>
|
||||
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #4251b0;
|
||||
}
|
||||
.st1 {
|
||||
fill: #fa2921;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
in:fade={{ duration: 100 }}
|
||||
out:fade={{ duration: 100 }}
|
||||
id="notification-panel"
|
||||
class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray text-light"
|
||||
class="absolute right-[25px] top-[70px] z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray text-light px-2"
|
||||
use:focusTrap
|
||||
>
|
||||
<Stack class="max-h-[500px]">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="ts">
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createProfileImage, type AssetResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import domtoimage from 'dom-to-image';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
|
@ -89,16 +88,17 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal title={$t('set_profile_picture')} width="auto" {onClose}>
|
||||
<div class="flex place-items-center items-center justify-center">
|
||||
<div
|
||||
class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
|
||||
>
|
||||
<PhotoViewer bind:element={imgElement} {asset} />
|
||||
<Modal size="small" title={$t('set_profile_picture')} {onClose}>
|
||||
<ModalBody>
|
||||
<div class="flex place-items-center items-center justify-center">
|
||||
<div
|
||||
class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
|
||||
>
|
||||
<PhotoViewer bind:element={imgElement} {asset} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button fullWidth shape="round" onclick={handleSetProfilePicture}>{$t('set_as_profile_picture')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@
|
|||
class={[
|
||||
{ 'border-b-2': isDragging },
|
||||
{ 'rounded-bl-md': !isDragging },
|
||||
'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg',
|
||||
'bg-light truncate opacity-85 pointer-events-none absolute end-0 min-w-20 max-w-64 w-fit rounded-ss-md border-immich-primary py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg z-1',
|
||||
]}
|
||||
style:top="{hoverY + 2}px"
|
||||
>
|
||||
|
|
@ -506,7 +506,7 @@
|
|||
{#if assetStore.scrolling && scrollHoverLabel && !isHover}
|
||||
<p
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/70 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg"
|
||||
class="truncate pointer-events-none absolute end-0 bottom-0 min-w-20 max-w-64 w-fit rounded-tl-md border-b-2 border-immich-primary bg-subtle/90 z-1 py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:text-immich-dark-fg"
|
||||
>
|
||||
{scrollHoverLabel}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="border rounded-2xl my-4 px-6 py-4 transition-all {isOpen
|
||||
? 'border-primary/40 dark:border-primary/50 shadow-md'
|
||||
class="border-2 rounded-2xl border-primary/20 my-4 px-6 py-4 transition-all {isOpen
|
||||
? 'border-primary/60 shadow-md'
|
||||
: ''}"
|
||||
bind:this={accordionElement}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import type { ServerVersionResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
|
||||
let showModal = $state(false);
|
||||
|
||||
|
|
@ -39,33 +38,38 @@
|
|||
</script>
|
||||
|
||||
{#if showModal}
|
||||
<FullScreenModal title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)}>
|
||||
<div>
|
||||
<FormatMessage key="version_announcement_message">
|
||||
{#snippet children({ tag, message })}
|
||||
{#if tag === 'link'}
|
||||
<span class="font-medium underline">
|
||||
<a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
</span>
|
||||
{:else if tag === 'code'}
|
||||
<code>{message}</code>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</div>
|
||||
<Modal size="small" title="🎉 {$t('new_version_available')}" onClose={() => (showModal = false)} icon={false}>
|
||||
<ModalBody>
|
||||
<div>
|
||||
<FormatMessage key="version_announcement_message">
|
||||
{#snippet children({ tag, message })}
|
||||
{#if tag === 'link'}
|
||||
<span class="font-medium underline">
|
||||
<a
|
||||
href="https://github.com/immich-app/immich/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
</span>
|
||||
{:else if tag === 'code'}
|
||||
<code>{message}</code>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
|
||||
<div class="mt-4 font-medium">{$t('version_announcement_closing')}</div>
|
||||
|
||||
<div class="font-sm mt-8">
|
||||
<code>{$t('server_version')}: {serverVersion}</code>
|
||||
<br />
|
||||
<code>{$t('latest_version')}: {releaseVersion}</code>
|
||||
</div>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<div class="font-sm mt-8">
|
||||
<code>{$t('server_version')}: {serverVersion}</code>
|
||||
<br />
|
||||
<code>{$t('latest_version')}: {releaseVersion}</code>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button fullWidth shape="round" onclick={onAcknowledge}>{$t('acknowledge')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue