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:
Alex 2025-05-19 09:32:23 -05:00 committed by GitHub
parent 2431e04a09
commit c8641d24f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 871 additions and 928 deletions

View file

@ -30,7 +30,7 @@
<div class="relative mx-auto font-mono text-2xl font-semibold">
<span class="text-gray-400 dark:text-gray-600">{zeros()}</span><span>{value}</span>
{#if unit}
<Code color="muted" class="absolute -top-5 end-2 font-light">{unit}</Code>
<Code color="muted" class="absolute -top-5 end-1 font-light">{unit}</Code>
{/if}
</div>
</div>

View file

@ -1,13 +1,12 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
import { handleError } from '$lib/utils/handle-error';
import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody } from '@immich/ui';
import { mdiEyeOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
@ -112,15 +111,17 @@
</div>
{#if htmlPreview}
<FullScreenModal title={$t('admin.template_email_preview')} onClose={closePreviewModal} width="wide">
<div style="position:relative; width:100%; height:90vh; overflow: hidden">
<iframe
title={$t('admin.template_email_preview')}
srcdoc={htmlPreview}
style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;"
></iframe>
</div>
</FullScreenModal>
<Modal title={$t('admin.template_email_preview')} onClose={closePreviewModal} size="medium">
<ModalBody>
<div style="position:relative; width:100%; height:90vh; overflow: hidden">
<iframe
title={$t('admin.template_email_preview')}
srcdoc={htmlPreview}
style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;"
></iframe>
</div>
</ModalBody>
</Modal>
{/if}
</form>
</div>

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { clickOutside } from '$lib/actions/click-outside';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import type Map from '$lib/components/shared-components/map/map.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { timeToLoadTheMap } from '$lib/constants';
@ -11,7 +10,7 @@
import { delay } from '$lib/utils/asset-utils';
import { navigate } from '$lib/utils/navigation';
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui';
import { LoadingSpinner, Modal, ModalBody } from '@immich/ui';
import { mdiMapOutline } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -112,31 +111,33 @@
{#if albumMapViewManager.isInMapView}
<div use:clickOutside={{ onOutclick: closeMap }}>
<FullScreenModal title={$t('map')} width="wide" onClose={closeMap}>
<div class="flex flex-col w-full h-full gap-2">
<div class="h-[500px] min-h-[300px] w-full">
{#await import('../shared-components/map/map.svelte')}
{#await delay(timeToLoadTheMap) then}
<!-- show the loading spinner only if loading the map takes too much time -->
<div class="flex items-center justify-center h-full w-full">
<LoadingSpinner />
</div>
<Modal title={$t('map')} size="medium" onClose={closeMap}>
<ModalBody>
<div class="flex flex-col w-full h-full gap-2 border border-gray-300 dark:border-light rounded-2xl">
<div class="h-[500px] min-h-[300px] w-full">
{#await import('../shared-components/map/map.svelte')}
{#await delay(timeToLoadTheMap) then}
<!-- show the loading spinner only if loading the map takes too much time -->
<div class="flex items-center justify-center h-full w-full">
<LoadingSpinner />
</div>
{/await}
{:then { default: Map }}
<Map
bind:this={mapElement}
center={undefined}
{zoom}
clickable={false}
bind:mapMarkers
onSelect={onViewAssets}
showSettings={false}
rounded
/>
{/await}
{:then { default: Map }}
<Map
bind:this={mapElement}
center={undefined}
{zoom}
clickable={false}
bind:mapMarkers
onSelect={onViewAssets}
showSettings={false}
rounded
/>
{/await}
</div>
</div>
</div>
</FullScreenModal>
</ModalBody>
</Modal>
</div>
<Portal target="body">

View file

@ -2,7 +2,6 @@
import Icon from '$lib/components/elements/icon.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import ConfirmModal from '$lib/modals/ConfirmModal.svelte';
@ -16,6 +15,7 @@
type AlbumResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Modal, ModalBody } from '@immich/ui';
import { mdiArrowDownThin, mdiArrowUpThin, mdiDotsVertical, mdiPlus } from '@mdi/js';
import { findKey } from 'lodash-es';
import { t } from 'svelte-i18n';
@ -115,79 +115,81 @@
</script>
{#if !selectedRemoveUser}
<FullScreenModal title={$t('options')} {onClose}>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title={$t('display_order')}
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
<Modal title={$t('options')} {onClose} size="small">
<ModalBody>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title={$t('display_order')}
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled}
onToggle={onToggleEnabledActivity}
/>
{/if}
<SettingSwitch
title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled}
onToggle={onToggleEnabledActivity}
/>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>{$t('invite_people')}</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
<div>{$t('owner')}</div>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2">
<button type="button" class="flex items-center gap-2" onclick={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div>
</div>
<div>{$t('invite_people')}</div>
</button>
{#each album.albumUsers as { user, role } (user.id)}
<div class="flex items-center gap-2 py-2">
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
{#if role === AlbumUserRole.Viewer}
{$t('role_viewer')}
{:else}
{$t('role_editor')}
{/if}
{#if user.id !== album.ownerId}
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
{#if role === AlbumUserRole.Viewer}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
text={$t('allow_edits')}
/>
{:else}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
text={$t('disallow_edits')}
/>
{/if}
<!-- Allow deletion for non-owners -->
<MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} />
</ButtonContextMenu>
{/if}
<div>{$t('owner')}</div>
</div>
{/each}
{#each album.albumUsers as { user, role } (user.id)}
<div class="flex items-center gap-2 py-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
{#if role === AlbumUserRole.Viewer}
{$t('role_viewer')}
{:else}
{$t('role_editor')}
{/if}
{#if user.id !== album.ownerId}
<ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}>
{#if role === AlbumUserRole.Viewer}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Editor)}
text={$t('allow_edits')}
/>
{:else}
<MenuOption
onClick={() => handleUpdateSharedUserRole(user, AlbumUserRole.Viewer)}
text={$t('disallow_edits')}
/>
{/if}
<!-- Allow deletion for non-owners -->
<MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} />
</ButtonContextMenu>
{/if}
</div>
{/each}
</div>
</div>
</div>
</div>
</FullScreenModal>
</ModalBody>
</Modal>
{/if}
{#if selectedRemoveUser}

View file

@ -40,7 +40,7 @@
onblur={handleUpdateName}
class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
? 'hover:border-gray-400'
: 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
: 'hover:border-transparent'} focus:border-b-2 focus:border-immich-primary focus:outline-none bg-light dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray placeholder:text-primary/90"
type="text"
bind:value={newAlbumName}
disabled={!isOwned}

View file

@ -50,7 +50,7 @@
{#each tags as tag (tag.id)}
<div class="flex group transition-all">
<a
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
href={encodeURI(`${AppRoute.TAGS}/?path=${tag.value}`)}
>
<p class="text-sm">

View file

@ -1,9 +1,8 @@
<script lang="ts">
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { handleError } from '$lib/utils/handle-error';
import { updateAlbumInfo, type AlbumResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -47,29 +46,33 @@
};
</script>
<FullScreenModal icon={mdiRenameOutline} title={$t('edit_album')} width="wide" {onClose}>
<form {onsubmit} autocomplete="off" id="edit-album-form">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" />
</div>
<div class="grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
<Modal icon={mdiRenameOutline} title={$t('edit_album')} size="medium" {onClose}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="edit-album-form">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} class="h-[200px] w-[200px] m-4 shadow-lg" />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">{$t('description')}</label>
<textarea class="immich-form-input" id="description" bind:value={description}></textarea>
<div class="grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">{$t('name')}</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">{$t('description')}</label>
<textarea class="immich-form-input" id="description" bind:value={description}></textarea>
</div>
</div>
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={() => onCancel?.()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button>
</div>
</form>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={() => onCancel?.()}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth disabled={isSubmitting} form="edit-album-form">{$t('save')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View file

@ -1,9 +1,8 @@
<script lang="ts">
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
interface Props {
exclusionPattern: string;
@ -42,37 +41,40 @@
};
</script>
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')}
<br /><br />
{$t('admin.add_exclusion_pattern_description')}
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
<Modal size="small" title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')}
<br /><br />
{$t('admin.add_exclusion_pattern_description')}
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
{/if}
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form"
>{submitText}</Button
>
</div>
</form>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="add-exclusion-pattern-form"
>{submitText}</Button
>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { Button } from '@immich/ui';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -46,29 +45,33 @@
};
</script>
<FullScreenModal {title} icon={mdiFolderSync} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<Modal {title} icon={mdiFolderSync} onClose={onCancel} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('path')}</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('path')}</label>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">{$t('errors.import_path_already_exists')}</p>
{/if}
</div>
</form>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{cancelText}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form"
>{submitText}</Button
>
</div>
</form>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{cancelText}</Button>
{#if isEditing}
<Button shape="round" color="danger" fullWidth onclick={onDelete}>{$t('delete')}</Button>
{/if}
<Button shape="round" type="submit" disabled={!canSubmit} fullWidth form="library-import-path-form"
>{submitText}</Button
>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View file

@ -1,7 +1,6 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import type { LibraryResponseDto } from '@immich/sdk';
import { Button, Field, Input } from '@immich/ui';
import { Button, Field, Input, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiRenameOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -21,15 +20,19 @@
};
</script>
<form {onsubmit} autocomplete="off">
<FullScreenModal icon={mdiRenameOutline} title={$t('rename')} onClose={onCancel}>
<Field label={$t('name')}>
<Input bind:value={newName} />
</Field>
<Modal icon={mdiRenameOutline} title={$t('rename')} onClose={onCancel} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="rename-library-form">
<Field label={$t('name')}>
<Input bind:value={newName} />
</Field>
</form>
</ModalBody>
{#snippet stickyBottom()}
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
<Button shape="round" fullWidth type="submit">{$t('save')}</Button>
{/snippet}
</FullScreenModal>
</form>
<Button shape="round" fullWidth type="submit" form="rename-library-form">{$t('save')}</Button>
</div>
</ModalFooter>
</Modal>

View file

@ -2,11 +2,10 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { user } from '$lib/stores/user.store';
import { searchUsersAdmin } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiFolderSync } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
interface Props {
onCancel: () => void;
@ -30,15 +29,19 @@
};
</script>
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="select-library-owner-form">
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
<Modal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel} size="small">
<ModalBody>
<form {onsubmit} autocomplete="off" id="select-library-owner-form">
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
</form>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
</form>
</ModalBody>
{#snippet stickyBottom()}
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button>
{/snippet}
</FullScreenModal>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button shape="round" color="secondary" fullWidth onclick={onCancel}>{$t('cancel')}</Button>
<Button shape="round" type="submit" fullWidth form="select-library-owner-form">{$t('create')}</Button>
</div>
</ModalFooter>
</Modal>

View file

@ -1,13 +1,12 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiClose, mdiTag } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
interface Props {
onTag: (tagIds: string[]) => void;
@ -52,48 +51,52 @@
};
</script>
<FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
onSelect={handleSelect}
label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>
<Modal size="small" title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<ModalBody>
<form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
onSelect={handleSelect}
label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>
</div>
</form>
<section class="flex flex-wrap pt-2 gap-1">
{#each selectedIds as tagId (tagId)}
{@const tag = tagMap[tagId]}
{#if tag}
<div class="flex group transition-all">
<span
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
>
<p class="text-sm">
{tag.value}
</p>
</span>
<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
title="Remove tag"
onclick={() => handleRemove(tagId)}
>
<Icon path={mdiClose} />
</button>
</div>
{/if}
{/each}
</section>
</ModalBody>
<ModalFooter>
<div class="flex w-full gap-2">
<Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
<Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
</div>
</form>
<section class="flex flex-wrap pt-2 gap-1">
{#each selectedIds as tagId (tagId)}
{@const tag = tagMap[tagId]}
{#if tag}
<div class="flex group transition-all">
<span
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary roudned-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
>
<p class="text-sm">
{tag.value}
</p>
</span>
<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
title="Remove tag"
onclick={() => handleRemove(tagId)}
>
<Icon path={mdiClose} />
</button>
</div>
{/if}
{/each}
</section>
{#snippet stickyBottom()}
<Button shape="round" fullWidth color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
<Button type="submit" shape="round" fullWidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
{/snippet}
</FullScreenModal>
</ModalFooter>
</Modal>

View file

@ -314,8 +314,9 @@
/>
{#if assetInteraction.selectionActive}
<div class="sticky top-0">
<div class="sticky top-0 z-1">
<AssetSelectControlBar
forceDark
assets={assetInteraction.selectedAssets}
clearSelect={() => cancelMultiselect(assetInteraction)}
>
@ -605,6 +606,7 @@
</section>
{/if}
</section>
{#if current}
<!-- GALLERY VIEWER -->
<section class="bg-immich-dark-gray p-4">

View file

@ -24,9 +24,10 @@
clearSelect: () => void;
ownerId?: string | undefined;
children?: Snippet;
forceDark?: boolean;
}
let { assets, clearSelect, ownerId = undefined, children }: Props = $props();
let { assets, clearSelect, ownerId = undefined, children, forceDark }: Props = $props();
setContext({
getAssets: () => assets,
@ -35,9 +36,11 @@
});
</script>
<ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<ControlAppBar onClose={clearSelect} {forceDark} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
{#snippet leading()}
<div class="font-medium text-immich-primary dark:text-immich-dark-primary">
<div
class="font-medium {forceDark ? 'text-immich-dark-primary' : 'text-immich-primary dark:text-immich-dark-primary'}"
>
<p class="block sm:hidden">{assets.length}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.length } })}</p>
</div>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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]">

View file

@ -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>

View file

@ -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>

View file

@ -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}
>

View file

@ -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}

View file

@ -1,9 +1,8 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants';
import { Button } from '@immich/ui';
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
import {
mdiArrowDownThin,
mdiArrowUpThin,
@ -65,37 +64,40 @@
};
</script>
<FullScreenModal title={$t('slideshow_settings')} onClose={() => onClose()}>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title={$t('direction')}
options={Object.values(navigationOptions)}
selectedOption={navigationOptions[tempSlideshowNavigation]}
onToggle={(option) => {
tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation;
}}
/>
<SettingDropdown
title={$t('look')}
options={Object.values(lookOptions)}
selectedOption={lookOptions[tempSlideshowLook]}
onToggle={(option) => {
tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook;
}}
/>
<SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} />
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('duration')}
description={$t('admin.slideshow_duration_description')}
min={1}
bind:value={tempSlideshowDelay}
/>
</div>
{#snippet stickyBottom()}
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button>
{/snippet}
</FullScreenModal>
<Modal size="small" title={$t('slideshow_settings')} onClose={() => onClose()}>
<ModalBody>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title={$t('direction')}
options={Object.values(navigationOptions)}
selectedOption={navigationOptions[tempSlideshowNavigation]}
onToggle={(option) => {
tempSlideshowNavigation = handleToggle(option, navigationOptions) || tempSlideshowNavigation;
}}
/>
<SettingDropdown
title={$t('look')}
options={Object.values(lookOptions)}
selectedOption={lookOptions[tempSlideshowLook]}
onToggle={(option) => {
tempSlideshowLook = handleToggle(option, lookOptions) || tempSlideshowLook;
}}
/>
<SettingSwitch title={$t('show_progress_bar')} bind:checked={tempShowProgressBar} />
<SettingSwitch title={$t('show_slideshow_transition')} bind:checked={tempSlideshowTransition} />
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('duration')}
description={$t('admin.slideshow_duration_description')}
min={1}
bind:value={tempSlideshowDelay}
/>
</div>
</ModalBody>
<ModalFooter>
<div class="flex gap-2 w-full">
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
<Button fullWidth color="primary" shape="round" onclick={applyChanges}>{$t('confirm')}</Button>
</div>
</ModalFooter>
</Modal>

View file

@ -126,7 +126,7 @@
maxlength="1"
bind:this={pinCodeInputElements[index]}
id="pin-code-{index}"
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 bg-transparent text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light"
class="h-12 w-10 rounded-xl border-2 border-suble dark:border-gray-700 text-center text-lg font-medium focus:border-immich-primary focus:ring-primary dark:focus:border-primary font-mono bg-white dark:bg-light"
bind:value={pinValues[index]}
onkeydown={handleKeydown}
oninput={(event) => handleInput(event, index)}