chore(web): migrate CircleIconButton to @immich/ui IconButton (#18486)

* remove import and referenced file

* first pass at replacing all CircleIconButtons

* fix linting issues

* fix combobox formatting issues

* fix button context menu coloring

* remove circle icon button from search history box

* use theme switcher from UI lib

* dark mode force the asset viewer icons

* fix forced dark mode icons

* dark mode memory viewer icons

* fix: back button in memory viewer

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Brandon Wees
2025-06-02 09:47:23 -05:00
committed by GitHub
parent d544053c67
commit a02e1f5e7c
75 changed files with 822 additions and 556 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { IconButton } from '@immich/ui';
import { mdiArrowLeft } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -13,4 +13,11 @@
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
<CircleIconButton color="opaque" icon={mdiArrowLeft} title={$t('go_back')} onclick={onClose} />
<IconButton
color="secondary"
variant="ghost"
shape="round"
icon={mdiArrowLeft}
aria-label={$t('go_back')}
onclick={onClose}
/>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
import {
NotificationType,
@@ -16,6 +15,7 @@
import { mdiDeleteForeverOutline, mdiDeleteOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction, PreAction } from './action';
import { IconButton } from '@immich/ui';
interface Props {
asset: AssetResponseDto;
@@ -81,10 +81,12 @@
]}
/>
<CircleIconButton
color="opaque"
<IconButton
color="secondary"
shape="round"
variant="ghost"
icon={asset.isTrashed ? mdiDeleteForeverOutline : mdiDeleteOutline}
title={asset.isTrashed ? $t('permanently_delete') : $t('delete')}
aria-label={asset.isTrashed ? $t('permanently_delete') : $t('delete')}
onclick={() => trashOrDelete(asset.isTrashed)}
/>

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { downloadFile } from '$lib/utils/asset-utils';
import { getAssetInfo } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiFolderDownloadOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -22,7 +22,13 @@
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
{#if !menuItem}
<CircleIconButton color="opaque" icon={mdiFolderDownloadOutline} title={$t('download')} onclick={onDownloadFile} />
<IconButton
color="primary"
shape="round"
icon={mdiFolderDownloadOutline}
aria-label={$t('download')}
onclick={onDownloadFile}
/>
{:else}
<MenuOption icon={mdiFolderDownloadOutline} text={$t('download')} onClick={onDownloadFile} />
{/if}

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import {
NotificationType,
notificationController,
@@ -12,6 +11,7 @@
import { mdiHeart, mdiHeartOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction } from './action';
import { IconButton } from '@immich/ui';
interface Props {
asset: AssetResponseDto;
@@ -48,9 +48,11 @@
<svelte:document use:shortcut={{ shortcut: { key: 'f' }, onShortcut: toggleFavorite }} />
<CircleIconButton
color="opaque"
<IconButton
color="secondary"
shape="round"
variant="ghost"
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
title={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')}
aria-label={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')}
onclick={toggleFavorite}
/>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { IconButton } from '@immich/ui';
import { mdiMotionPauseOutline, mdiPlaySpeed } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -11,9 +11,10 @@
let { isPlaying, onClick }: Props = $props();
</script>
<CircleIconButton
color="opaque"
<IconButton
shape="round"
color="primary"
icon={isPlaying ? mdiMotionPauseOutline : mdiPlaySpeed}
title={isPlaying ? $t('stop_motion_photo') : $t('play_motion_photo')}
aria-label={isPlaying ? $t('stop_motion_photo') : $t('play_motion_photo')}
onclick={() => onClick(!isPlaying)}
/>

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
import type { AssetResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiShareVariantOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -23,4 +23,11 @@
};
</script>
<CircleIconButton color="opaque" icon={mdiShareVariantOutline} onclick={handleClick} title={$t('share')} />
<IconButton
color="secondary"
shape="round"
variant="ghost"
icon={mdiShareVariantOutline}
onclick={handleClick}
aria-label={$t('share')}
/>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { IconButton } from '@immich/ui';
import { mdiInformationOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -13,4 +13,11 @@
<svelte:document use:shortcut={{ shortcut: { key: 'i' }, onShortcut: onShowDetail }} />
<CircleIconButton color="opaque" icon={mdiInformationOutline} onclick={onShowDetail} title={$t('info')} />
<IconButton
color="secondary"
shape="round"
variant="ghost"
icon={mdiInformationOutline}
onclick={onShowDetail}
aria-label={$t('info')}
/>

View File

@@ -12,10 +12,10 @@
import { handleError } from '$lib/utils/handle-error';
import { isTenMinutesApart } from '$lib/utils/timesince';
import { ReactionType, type ActivityResponseDto, type AssetTypeEnum, type UserResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiClose, mdiDeleteOutline, mdiDotsVertical, mdiHeart, mdiSend } from '@mdi/js';
import * as luxon from 'luxon';
import { t } from 'svelte-i18n';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import UserAvatar from '../shared-components/user-avatar.svelte';
@@ -125,7 +125,14 @@
bind:clientHeight={activityHeight}
>
<div class="flex place-items-center gap-2">
<CircleIconButton onclick={onClose} icon={mdiClose} title={$t('close')} />
<IconButton
shape="round"
variant="ghost"
color="secondary"
onclick={onClose}
icon={mdiClose}
aria-label={$t('close')}
/>
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('activity')}</p>
</div>
@@ -159,7 +166,7 @@
title={$t('comment_options')}
align="top-right"
direction="left"
size="16"
size="small"
>
<MenuOption
activeColor="bg-red-200"
@@ -212,7 +219,7 @@
title={$t('reaction_options')}
align="top-right"
direction="left"
size="16"
size="small"
>
<MenuOption
activeColor="bg-red-200"
@@ -269,9 +276,11 @@
</div>
{:else if message}
<div class="flex items-end w-fit ms-0">
<CircleIconButton
title={$t('send_message')}
size="15"
<IconButton
shape="round"
aria-label={$t('send_message')}
size="small"
variant="ghost"
icon={mdiSend}
class="dark:text-immich-dark-gray"
onclick={() => handleSendComment()}

View File

@@ -17,7 +17,6 @@
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.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 { AppRoute } from '$lib/constants';
@@ -36,6 +35,7 @@
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
import { IconButton } from '@immich/ui';
import {
mdiAlertOutline,
mdiCogRefreshOutline,
@@ -111,34 +111,49 @@
<div
class="flex h-16 place-items-center justify-between bg-linear-to-b from-black/40 px-3 transition-transform duration-200"
>
<div class="text-white">
<div class="dark">
{#if showCloseButton}
<CloseAction {onClose} />
{/if}
</div>
<div class="flex gap-2 overflow-x-auto text-white" data-testid="asset-viewer-navbar-actions">
<div class="flex gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions">
<CastButton />
{#if !asset.isTrashed && $user && !isLocked}
<ShareAction {asset} />
{/if}
{#if asset.isOffline}
<CircleIconButton color="alert" icon={mdiAlertOutline} onclick={onShowDetail} title={$t('asset_offline')} />
<IconButton
shape="round"
color="danger"
icon={mdiAlertOutline}
onclick={onShowDetail}
aria-label={$t('asset_offline')}
/>
{/if}
{#if asset.livePhotoVideoId}
{@render motionPhoto?.()}
{/if}
{#if asset.type === AssetTypeEnum.Image}
<CircleIconButton
color="opaque"
hideMobile={true}
<IconButton
class="hidden sm:flex"
color="secondary"
variant="ghost"
shape="round"
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
title={$t('zoom_image')}
aria-label={$t('zoom_image')}
onclick={onZoomImage}
/>
{/if}
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image}
<CircleIconButton color="opaque" icon={mdiContentCopy} title={$t('copy_image')} onclick={() => onCopyImage?.()} />
<IconButton
color="secondary"
variant="ghost"
shape="round"
icon={mdiContentCopy}
aria-label={$t('copy_image')}
onclick={() => onCopyImage?.()}
/>
{/if}
{#if !isOwner && showDownloadButton}
@@ -152,20 +167,11 @@
{#if isOwner}
<FavoriteAction {asset} {onAction} />
{/if}
<!-- {#if showEditorButton}
<CircleIconButton
color="opaque"
hideMobile={true}
icon={mdiImageEditOutline}
onclick={showEditorHandler}
title={$t('editor')}
/>
{/if} -->
{#if isOwner}
<DeleteAction {asset} {onAction} {preAction} />
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
<ButtonContextMenu direction="left" align="top-right" color="secondary" title={$t('more')} icon={mdiDotsVertical}>
{#if showSlideshow && !isLocked}
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
{/if}

View File

@@ -27,6 +27,7 @@
type AssetResponseDto,
type ExifResponseDto,
} from '@immich/sdk';
import { IconButton } from '@immich/ui';
import {
mdiCalendar,
mdiCameraIris,
@@ -42,7 +43,6 @@
import { t } from 'svelte-i18n';
import { slide } from 'svelte/transition';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import PersonSidePanel from '../faces-page/person-side-panel.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
@@ -158,7 +158,14 @@
<section class="relative p-2">
<div class="flex place-items-center gap-2">
<CircleIconButton icon={mdiClose} title={$t('close')} onclick={onClose} />
<IconButton
icon={mdiClose}
aria-label={$t('close')}
onclick={onClose}
shape="round"
color="secondary"
variant="ghost"
/>
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
</div>
@@ -193,30 +200,34 @@
<h2>{$t('people').toUpperCase()}</h2>
<div class="flex gap-2 items-center">
{#if people.some((person) => person.isHidden)}
<CircleIconButton
title={$t('show_hidden_people')}
<IconButton
aria-label={$t('show_hidden_people')}
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
padding="1"
buttonSize="32"
size="medium"
shape="round"
color="secondary"
variant="ghost"
onclick={() => (showingHiddenPeople = !showingHiddenPeople)}
/>
{/if}
<CircleIconButton
title={$t('tag_people')}
<IconButton
aria-label={$t('tag_people')}
icon={mdiPlus}
padding="1"
size="20"
buttonSize="32"
size="medium"
shape="round"
color="secondary"
variant="ghost"
onclick={() => (isFaceEditMode.value = !isFaceEditMode.value)}
/>
{#if people.length > 0 || unassignedFaces.length > 0}
<CircleIconButton
title={$t('edit_people')}
<IconButton
aria-label={$t('edit_people')}
icon={mdiPencil}
padding="1"
size="20"
buttonSize="32"
size="medium"
shape="round"
color="secondary"
variant="ghost"
onclick={() => (showEditFaces = true)}
/>
{/if}
@@ -369,11 +380,13 @@
<p class="break-all flex place-items-center gap-2 whitespace-pre-wrap">
{asset.originalFileName}
{#if isOwner}
<CircleIconButton
<IconButton
icon={mdiInformationOutline}
title={$t('show_file_location')}
size="16"
padding="2"
aria-label={$t('show_file_location')}
size="small"
shape="round"
color="secondary"
variant="ghost"
onclick={toggleAssetPath}
/>
{/if}

View File

@@ -5,7 +5,7 @@
import { t } from 'svelte-i18n';
import { fly, slide } from 'svelte/transition';
import { getByteUnitString } from '../../utils/byte-units';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { IconButton } from '@immich/ui';
const abort = (downloadKey: string, download: DownloadProgress) => {
download.abort?.abort();
@@ -42,10 +42,13 @@
</div>
</div>
<div class="absolute end-2">
<CircleIconButton
title={$t('close')}
<IconButton
variant="ghost"
shape="round"
color="secondary"
aria-label={$t('close')}
onclick={() => abort(downloadKey, download)}
size="20"
size="large"
icon={mdiClose}
class="dark:text-immich-dark-gray"
/>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import {
cropAspectRatio,
cropImageScale,
@@ -10,11 +9,12 @@
rotateDegrees,
type CropAspectRatio,
} from '$lib/stores/asset-editor.store';
import { IconButton } from '@immich/ui';
import { mdiBackupRestore, mdiCropFree, mdiRotateLeft, mdiRotateRight, mdiSquareOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import { onImageLoad } from './image-loading';
import { tick } from 'svelte';
import { t } from 'svelte-i18n';
import CropPreset from './crop-preset.svelte';
import { onImageLoad } from './image-loading';
let rotateHorizontal = $derived([90, 270].includes($normaizedRorateDegrees));
const icon_16_9 = `M200-280q-33 0-56.5-23.5T120-360v-240q0-33 23.5-56.5T200-680h560q33 0 56.5 23.5T840-600v240q0 33-23.5 56.5T760-280H200Zm0-80h560v-240H200v240Zm0 0v-240 240Z`;
@@ -148,7 +148,25 @@
<h2>{$t('editor_crop_tool_h2_rotation').toUpperCase()}</h2>
</div>
<ul class="flex-wrap flex-row flex gap-x-6 gap-y-4 justify-center">
<li><CircleIconButton title={$t('anti_clockwise')} onclick={() => rotate(false)} icon={mdiRotateLeft} /></li>
<li><CircleIconButton title={$t('clockwise')} onclick={() => rotate(true)} icon={mdiRotateRight} /></li>
<li>
<IconButton
shape="round"
variant="ghost"
color="secondary"
aria-label={$t('anti_clockwise')}
onclick={() => rotate(false)}
icon={mdiRotateLeft}
/>
</li>
<li>
<IconButton
shape="round"
variant="ghost"
color="secondary"
aria-label={$t('clockwise')}
onclick={() => rotate(true)}
icon={mdiRotateRight}
/>
</li>
</ul>
</div>

View File

@@ -4,10 +4,10 @@
import { editTypes, showCancelConfirmDialog } from '$lib/stores/asset-editor.store';
import { websocketEvents } from '$lib/stores/websocket';
import { type AssetResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiClose } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import CircleIconButton from '../../elements/buttons/circle-icon-button.svelte';
onMount(() => {
return websocketEvents.on('on_asset_update', (assetUpdate) => {
@@ -44,17 +44,25 @@
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<div class="flex place-items-center gap-2">
<CircleIconButton icon={mdiClose} title={$t('close')} onclick={onClose} />
<IconButton
shape="round"
variant="ghost"
color="secondary"
icon={mdiClose}
aria-label={$t('close')}
onclick={onClose}
/>
<p class="text-lg text-immich-fg dark:text-immich-dark-fg capitalize">{$t('editor')}</p>
</div>
<section class="px-4 py-4">
<ul class="flex w-full justify-around">
{#each editTypes as etype (etype.name)}
<li>
<CircleIconButton
color={etype.name === selectedType ? 'primary' : 'opaque'}
<IconButton
shape="round"
color={etype.name === selectedType ? 'primary' : 'secondary'}
icon={etype.icon}
title={etype.name}
aria-label={etype.name}
onclick={() => selectType(etype.name)}
/>
</li>

View File

@@ -132,7 +132,7 @@
{#if showControls}
<div
class="m-4 flex gap-2"
class="m-4 flex gap-2 dark"
onmouseenter={() => (isOverControls = true)}
onmouseleave={() => (isOverControls = false)}
transition:fly={{ duration: 150 }}
@@ -145,7 +145,6 @@
icon={mdiClose}
onclick={onClose}
aria-label={$t('exit_slideshow')}
class="text-white"
/>
<IconButton
@@ -155,7 +154,6 @@
icon={progressBarStatus === ProgressBarStatus.Paused ? mdiPlay : mdiPause}
onclick={() => (progressBarStatus === ProgressBarStatus.Paused ? progressBar?.play() : progressBar?.pause())}
aria-label={progressBarStatus === ProgressBarStatus.Paused ? $t('play') : $t('pause')}
class="text-white"
/>
<IconButton
variant="ghost"
@@ -164,7 +162,6 @@
icon={mdiChevronLeft}
onclick={onPrevious}
aria-label={$t('previous')}
class="text-white"
/>
<IconButton
variant="ghost"
@@ -173,7 +170,6 @@
icon={mdiChevronRight}
onclick={onNext}
aria-label={$t('next')}
class="text-white"
/>
<IconButton
variant="ghost"
@@ -182,7 +178,6 @@
icon={mdiCog}
onclick={onShowSettings}
aria-label={$t('slideshow_settings')}
class="text-white"
/>
{#if !isFullScreen}
<IconButton
@@ -192,7 +187,6 @@
icon={mdiFullscreen}
onclick={onSetToFullScreen}
aria-label={$t('set_slideshow_to_fullscreen')}
class="text-white"
/>
{/if}
</div>

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { castManager, CastState } from '$lib/managers/cast-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { IconButton } from '@immich/ui';
import { mdiCastConnected, mdiPause, mdiPlay } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -84,11 +84,13 @@
<LoadingSpinner />
</div>
{:else}
<CircleIconButton
color="opaque"
<IconButton
color="primary"
shape="round"
variant="ghost"
icon={castManager.castState == CastState.PLAYING ? mdiPause : mdiPlay}
onclick={() => handlePlayPauseButton()}
title={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'}
aria-label={castManager.castState == CastState.PLAYING ? 'Pause' : 'Play'}
/>
{/if}