1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-17 01:06:46 +01:00

feat(web): increase usage of CircleIconButton (#9256)

This commit is contained in:
Ben 2024-05-04 18:29:50 +00:00 committed by GitHub
parent 5b87abb021
commit 48b490f5e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 166 additions and 177 deletions

View file

@ -1,6 +1,5 @@
<script lang="ts">
import Badge from '$lib/components/elements/badge.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { locale } from '$lib/stores/preferences.store';
import { JobCommand, type JobCommandDto, type JobCountsDto, type QueueStatusDto } from '@immich/sdk';
@ -16,6 +15,7 @@
import { createEventDispatcher } from 'svelte';
import JobTileButton from './job-tile-button.svelte';
import JobTileStatus from './job-tile-status.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let title: string;
export let subtitle: string | undefined;
@ -56,16 +56,19 @@
<div class="flex gap-2">
{#if jobCounts.failed > 0}
<Badge color="primary">
<span class="text-sm">
{jobCounts.failed.toLocaleString($locale)} failed
</span>
<Button
size="tiny"
shadow={false}
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
>
<Icon path={mdiClose} size="18" />
</Button>
<div class="flex flex-row gap-1">
<span class="text-sm">
{jobCounts.failed.toLocaleString($locale)} failed
</span>
<CircleIconButton
color="primary"
icon={mdiClose}
title="Clear message"
size="12"
padding="1"
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
/>
</div>
</Badge>
{/if}
{#if jobCounts.delayed > 0}

View file

@ -33,7 +33,7 @@
data-testid="context-button-parent"
>
<CircleIconButton
color="light"
color="opaque"
title="Show album options"
icon={mdiDotsVertical}
size="20"

View file

@ -19,7 +19,6 @@
class="w-full h-14 flex p-4 text-white items-center justify-center rounded-full gap-4 bg-immich-dark-bg bg-opacity-60"
>
<button class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
<!-- svelte-ignore missing-declaration -->
<div class="items-center justify-center">
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
</div>

View file

@ -160,12 +160,7 @@
bind:clientHeight={activityHeight}
>
<div class="flex place-items-center gap-2">
<button
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={() => dispatch('close')}
>
<Icon path={mdiClose} size="24" />
</button>
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} title="Close" />
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Activity</p>
</div>
@ -193,10 +188,13 @@
</a>
{/if}
{#if reaction.user.id === user.id || albumOwnerId === user.id}
<div class="flex items-start w-fit pt-[5px]" title="Delete comment">
<button on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}>
<Icon path={mdiDotsVertical} />
</button>
<div class="flex items-start w-fit pt-[5px]">
<CircleIconButton
icon={mdiDotsVertical}
title="Comment options"
size="16"
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
/>
</div>
{/if}
<div>
@ -242,10 +240,13 @@
</a>
{/if}
{#if reaction.user.id === user.id || albumOwnerId === user.id}
<div class="flex items-start w-fit" title="Delete like">
<button on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}>
<Icon path={mdiDotsVertical} />
</button>
<div class="flex items-start w-fit">
<CircleIconButton
icon={mdiDotsVertical}
title="Reaction options"
size="16"
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
/>
</div>
{/if}
<div>

View file

@ -169,13 +169,7 @@
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<div class="flex place-items-center gap-2">
<button
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={() => dispatch('close')}
>
<Icon path={mdiClose} size="24" />
</button>
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
</div>
@ -401,9 +395,13 @@
<p class="break-all flex place-items-center gap-2">
{asset.originalFileName}
{#if isOwner}
<button title="Show File Location" on:click={toggleAssetPath} class="-translate-y-[2px]">
<Icon path={mdiInformationOutline} />
</button>
<CircleIconButton
icon={mdiInformationOutline}
title="Show file location"
size="16"
padding="2"
on:click={toggleAssetPath}
/>
{/if}
</p>
<div class="flex gap-2 text-sm">

View file

@ -1,20 +1,23 @@
<script lang="ts">
import { mdiClose, mdiMagnify } from '@mdi/js';
import Icon from './icon.svelte';
import { createEventDispatcher } from 'svelte';
import type { SearchOptions } from '$lib/utils/dipatch';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let name: string;
export let roundedBottom = true;
export let showLoadingSpinner: boolean;
export let placeholder: string;
let inputRef: HTMLElement;
const dispatch = createEventDispatcher<{ search: SearchOptions; reset: void }>();
const resetSearch = () => {
name = '';
dispatch('reset');
inputRef?.focus();
};
const handleSearch = (event: KeyboardEvent) => {
@ -29,16 +32,19 @@
? 'rounded-2xl'
: 'rounded-t-lg'} bg-gray-200 p-2 dark:bg-immich-dark-gray gap-2 place-items-center h-full"
>
<button type="button" on:click={() => dispatch('search', { force: true })}>
<div class="w-fit">
<Icon path={mdiMagnify} size="24" />
</div>
</button>
<CircleIconButton
icon={mdiMagnify}
title="Search"
size="16"
padding="2"
on:click={() => dispatch('search', { force: true })}
/>
<input
class="w-full gap-2 bg-gray-200 dark:bg-immich-dark-gray dark:text-white"
type="text"
{placeholder}
bind:value={name}
bind:this={inputRef}
on:keydown={handleSearch}
on:input={() => dispatch('search', { force: false })}
/>
@ -48,8 +54,6 @@
</div>
{/if}
{#if name}
<button on:click={resetSearch}>
<Icon path={mdiClose} />
</button>
<CircleIconButton icon={mdiClose} title="Clear value" size="16" padding="2" on:click={resetSearch} />
{/if}
</div>

View file

@ -9,9 +9,9 @@
import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Icon from '../elements/icon.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let peopleWithFaces: AssetFaceResponseDto[];
export let allPeople: PersonResponseDto[];
@ -119,38 +119,19 @@
<div class="flex place-items-center justify-between gap-2">
{#if !searchFaces}
<div class="flex items-center gap-2">
<button
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={handleBackButton}
>
<div>
<Icon path={mdiArrowLeftThin} size="24" />
</div>
</button>
<CircleIconButton icon={mdiArrowLeftThin} title="Back" on:click={handleBackButton} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Select face</p>
</div>
<div class="flex justify-end gap-2">
<button
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
title="Search existing person"
<CircleIconButton
icon={mdiMagnify}
title="Search for existing person"
on:click={() => {
searchFaces = true;
}}
>
<div>
<Icon path={mdiMagnify} size="24" />
</div>
</button>
/>
{#if !isShowLoadingNewPerson}
<button
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={handleCreatePerson}
title="Create new person"
>
<div>
<Icon path={mdiPlus} size="24" />
</div>
</button>
<CircleIconButton icon={mdiPlus} title="Create new person" on:click={handleCreatePerson} />
{:else}
<div class="flex place-content-center place-items-center">
<LoadingSpinner />
@ -158,14 +139,7 @@
{/if}
</div>
{:else}
<button
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={handleBackButton}
>
<div>
<Icon path={mdiArrowLeftThin} size="24" />
</div>
</button>
<CircleIconButton icon={mdiArrowLeftThin} title="Back" on:click={handleBackButton} />
<div class="w-full flex">
<SearchPeople
type="input"
@ -179,14 +153,7 @@
</div>
{/if}
</div>
<button
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={() => (searchFaces = false)}
>
<div>
<Icon path={mdiClose} size="24" />
</div>
</button>
<CircleIconButton icon={mdiClose} title="Cancel search" on:click={() => (searchFaces = false)} />
{/if}
</div>
<div class="px-4 py-4 text-sm">

View file

@ -74,7 +74,7 @@
<div class="absolute right-2 top-2" class:hidden={!showVerticalDots}>
<CircleIconButton
color="light"
color="opaque"
icon={mdiDotsVertical}
title="Show person options"
size="20"

View file

@ -15,14 +15,14 @@
type AssetFaceResponseDto,
type PersonResponseDto,
} from '@immich/sdk';
import { mdiArrowLeftThin, mdiRestart } from '@mdi/js';
import { mdiArrowLeftThin, mdiMinus, mdiRestart } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Icon from '../elements/icon.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let assetId: string;
export let assetType: AssetTypeEnum;
@ -42,7 +42,7 @@
let isShowLoadingPeople = false;
// search people
let showSeletecFaces = false;
let showSelectedFaces = false;
let allPeople: PersonResponseDto[] = [];
// timers
@ -159,21 +159,21 @@
if (newFeaturePhoto && personToUpdate) {
selectedPersonToCreate[personToUpdate.id] = newFeaturePhoto;
}
showSeletecFaces = false;
showSelectedFaces = false;
};
const handleReassignFace = (person: PersonResponseDto | null) => {
const personToUpdate = peopleWithFaces.find((face) => face.person?.id === editedPerson.id);
if (person && personToUpdate) {
selectedPersonToReassign[personToUpdate.id] = person;
showSeletecFaces = false;
showSelectedFaces = false;
}
};
const handlePersonPicker = (person: PersonResponseDto | null) => {
if (person) {
editedPerson = person;
showSeletecFaces = true;
showSelectedFaces = true;
}
};
</script>
@ -184,14 +184,7 @@
>
<div class="flex place-items-center justify-between gap-2">
<div class="flex items-center gap-2">
<button
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
on:click={handleBackButton}
>
<div>
<Icon path={mdiArrowLeftThin} size="24" />
</div>
</button>
<CircleIconButton icon={mdiArrowLeftThin} title="Back" on:click={handleBackButton} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Edit faces</p>
</div>
{#if !isShowLoadingDone}
@ -273,21 +266,27 @@
</p>
{/if}
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700">
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full">
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
<button on:click={() => handleReset(face.id)} class="flex h-full w-full">
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<div>
<Icon path={mdiRestart} size={18} />
</div>
</div>
</button>
<CircleIconButton
color="primary"
icon={mdiRestart}
title="Reset"
size="18"
padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
on:click={() => handleReset(face.id)}
/>
{:else}
<button on:click={() => handlePersonPicker(face.person)} class="flex h-full w-full">
<div
class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white"
/>
</button>
<CircleIconButton
color="primary"
icon={mdiMinus}
title="Select new face"
size="18"
padding="1"
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
on:click={() => handlePersonPicker(face.person)}
/>
{/if}
</div>
</div>
@ -299,14 +298,14 @@
</div>
</section>
{#if showSeletecFaces}
{#if showSelectedFaces}
<AssignFaceSidePanel
{peopleWithFaces}
{allPeople}
{editedPerson}
{assetType}
{assetId}
on:close={() => (showSeletecFaces = false)}
on:close={() => (showSelectedFaces = false)}
on:createPerson={(event) => handleCreatePerson(event.detail)}
on:reassign={(event) => handleReassignFace(event.detail)}
/>

View file

@ -8,6 +8,7 @@
import { validate, type LibraryResponseDto } from '@immich/sdk';
import type { ValidateLibraryImportPathResponseDto } from '@immich/sdk';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let library: LibraryResponseDto;
@ -209,17 +210,17 @@
</td>
<td class="w-4/5 text-ellipsis px-4 text-sm">{validatedPath.importPath}</td>
<td class="w-1/5 text-ellipsis px-4 text-sm flex flex-row">
<button
type="button"
<td class="w-1/5 text-ellipsis flex justify-center">
<CircleIconButton
color="primary"
icon={mdiPencilOutline}
title="Edit import path"
size="16"
on:click={() => {
editImportPath = listIndex;
editedImportPath = validatedPath.importPath;
}}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
>
<Icon path={mdiPencilOutline} size="16" />
</button>
/>
</td>
</tr>
{/each}

View file

@ -1,11 +1,11 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { LibraryType, type LibraryResponseDto } from '@immich/sdk';
import { mdiPencilOutline } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte';
import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let library: Partial<LibraryResponseDto>;
@ -138,17 +138,17 @@
}`}
>
<td class="w-3/4 text-ellipsis px-4 text-sm">{exclusionPattern}</td>
<td class="w-1/4 text-ellipsis px-4 text-sm">
<button
type="button"
<td class="w-1/4 text-ellipsis flex justify-center">
<CircleIconButton
color="primary"
icon={mdiPencilOutline}
title="Edit exclusion pattern"
size="16"
on:click={() => {
editExclusionPattern = listIndex;
editedExclusionPattern = exclusionPattern;
}}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
>
<Icon path={mdiPencilOutline} size="16" />
</button>
/>
</td>
</tr>
{/each}

View file

@ -340,9 +340,12 @@
class:opacity-0={galleryInView}
class:opacity-100={!galleryInView}
>
<button on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}>
<CircleIconButton title="Show gallery" icon={mdiChevronDown} color="light" />
</button>
<CircleIconButton
title="Show gallery"
icon={mdiChevronDown}
color="light"
on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}
/>
</div>
<IntersectionObserver

View file

@ -12,6 +12,7 @@
import UserAvatar from '../user-avatar.svelte';
import AvatarSelector from './avatar-selector.svelte';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
let isShowSelectAvatar = false;
@ -61,15 +62,16 @@
{#key $user}
<UserAvatar user={$user} size="xl" />
{/key}
<div
class="absolute z-10 bottom-0 right-0 rounded-full w-6 h-6 border dark:border-immich-dark-primary bg-immich-primary"
>
<button
class="flex items-center justify-center w-full h-full text-white"
<div class="absolute z-10 bottom-0 right-0 rounded-full w-6 h-6">
<CircleIconButton
color="primary"
icon={mdiPencil}
title="Edit avatar"
class="border"
size="12"
padding="2"
on:click={() => (isShowSelectAvatar = true)}
>
<Icon path={mdiPencil} />
</button>
/>
</div>
</div>
<div>

View file

@ -8,6 +8,7 @@
} from '$lib/components/shared-components/notification/notification';
import { onMount } from 'svelte';
import { mdiCloseCircleOutline, mdiInformationOutline, mdiWindowClose } from '@mdi/js';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let notification: Notification;
@ -78,9 +79,14 @@
{notification.type.toString()}
</h2>
</div>
<button on:click|stopPropagation={discard}>
<Icon path={mdiWindowClose} size="20" />
</button>
<CircleIconButton
icon={mdiWindowClose}
title="Close"
class="dark:text-immich-dark-gray"
size="20"
padding="2"
on:click={discard}
/>
</div>
<p class="whitespace-pre-wrap pl-[28px] pr-[16px] text-sm" data-testid="message">

View file

@ -1,4 +1,5 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { locale } from '$lib/stores/preferences.store';
import type { SessionResponseDto } from '@immich/sdk';
@ -64,14 +65,14 @@
</div>
</div>
{#if !device.current}
<div class="flex flex-col justify-center text-sm">
<button
on:click={() => dispatcher('delete')}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
<div>
<CircleIconButton
color="primary"
icon={mdiTrashCanOutline}
title="Log out"
>
<Icon path={mdiTrashCanOutline} size="16" />
</button>
size="16"
on:click={() => dispatcher('delete')}
/>
</div>
{/if}
</div>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { locale } from '$lib/stores/preferences.store';
import { createApiKey, deleteApiKey, getApiKeys, updateApiKey, type ApiKeyResponseDto } from '@immich/sdk';
import { mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
@ -10,6 +9,7 @@
import APIKeySecret from '../forms/api-key-secret.svelte';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let keys: ApiKeyResponseDto[];
@ -143,19 +143,21 @@
<td class="w-1/3 text-ellipsis px-4 text-sm"
>{new Date(key.createdAt).toLocaleDateString($locale, format)}
</td>
<td class="w-1/3 text-ellipsis px-4 text-sm">
<button
<td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/3">
<CircleIconButton
color="primary"
icon={mdiPencilOutline}
title="Edit key"
size="16"
on:click={() => (editKey = key)}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
>
<Icon path={mdiPencilOutline} size="16" />
</button>
<button
/>
<CircleIconButton
color="primary"
icon={mdiTrashCanOutline}
title="Delete key"
size="16"
on:click={() => (deleteKey = key)}
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
>
<Icon path={mdiTrashCanOutline} size="16" />
</button>
/>
</td>
</tr>
{/key}

View file

@ -2,6 +2,7 @@
import { page } from '$app/stores';
import Icon from '$lib/components/elements/icon.svelte';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { copyToClipboard } from '$lib/utils';
import { mdiCodeTags, mdiContentCopy, mdiMessage, mdiPartyPopper } from '@mdi/js';
@ -36,12 +37,12 @@
🚨 Error - Something went wrong
</h1>
<div class="flex justify-end">
<button
<CircleIconButton
color="primary"
icon={mdiContentCopy}
title="Copy error"
on:click={() => handleCopy()}
class="rounded-full bg-immich-primary px-3 py-2 text-sm text-white shadow-md transition-colors hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80"
>
<Icon path={mdiContentCopy} size={24} />
</button>
/>
</div>
</div>

View file

@ -37,6 +37,7 @@
import { fade, slide } from 'svelte/transition';
import LinkButton from '../../../lib/components/elements/buttons/link-button.svelte';
import type { PageData } from './$types';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
export let data: PageData;
@ -386,12 +387,13 @@
{/if}
<td class=" text-ellipsis px-4 text-sm">
<button
class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
on:click|stopPropagation|preventDefault={(e) => showMenu(e, library, index)}
>
<Icon path={mdiDotsVertical} size="16" />
</button>
<CircleIconButton
color="primary"
icon={mdiDotsVertical}
title="Library options"
size="16"
on:click={(e) => showMenu(e, library, index)}
/>
{#if showContextMenu}
<Portal target="body">