From 3a2bf91889ddfeb0262f48228e0d845e506bce05 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jason@rasm.me>
Date: Thu, 16 Jan 2025 11:03:04 -0500
Subject: [PATCH] refactor: replace link-button component with immich-ui
 buttons (#15374)

* refactor: replace link-button component with immich-ui buttons

* minor styling tweak

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
---
 web/src/app.css                               |   2 +-
 .../album-page/albums-controls.svelte         |  90 ++++----
 .../elements/buttons/link-button.svelte       |  25 ---
 .../lib/components/elements/dropdown.svelte   |  26 +--
 .../map-page/map-settings-modal.svelte        | 198 ++++++++++--------
 .../create-shared-link-modal.svelte           |  35 ++--
 .../navigation-bar/navigation-bar.svelte      |  24 ++-
 web/src/routes/(user)/people/+page.svelte     |  19 +-
 web/src/routes/(user)/sharing/+page.svelte    |  34 ++-
 .../[[assetId=id]]/+page.svelte               |  53 ++---
 .../[[assetId=id]]/+page.svelte               |  48 +++--
 .../[[assetId=id]]/+page.svelte               |  57 ++---
 web/src/routes/admin/jobs-status/+page.svelte |  26 +--
 .../admin/library-management/+page.svelte     |  22 +-
 web/src/routes/admin/repair/+page.svelte      |  68 +++---
 .../routes/admin/system-settings/+page.svelte |  41 ++--
 16 files changed, 385 insertions(+), 383 deletions(-)
 delete mode 100644 web/src/lib/components/elements/buttons/link-button.svelte

diff --git a/web/src/app.css b/web/src/app.css
index 00cefc5ce6..7a547d3504 100644
--- a/web/src/app.css
+++ b/web/src/app.css
@@ -26,7 +26,7 @@
   :root {
     /* light */
     --immich-ui-primary: 66 80 175;
-    --immich-ui-dark: 0 0 0;
+    --immich-ui-dark: 58 58 58;
     --immich-ui-light: 255 255 255;
     --immich-ui-success: 34 197 94;
     --immich-ui-danger: 180 0 0;
diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte
index 85a7260f40..12b83a2b4c 100644
--- a/web/src/lib/components/album-page/albums-controls.svelte
+++ b/web/src/lib/components/album-page/albums-controls.svelte
@@ -1,15 +1,30 @@
 <script lang="ts">
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Dropdown from '$lib/components/elements/dropdown.svelte';
+  import GroupTab from '$lib/components/elements/group-tab.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
+  import SearchBar from '$lib/components/elements/search-bar.svelte';
   import {
     AlbumFilter,
-    AlbumSortBy,
     AlbumGroupBy,
+    AlbumSortBy,
     AlbumViewMode,
     albumViewSettings,
     SortOrder,
   } from '$lib/stores/preferences.store';
+  import {
+    type AlbumGroupOptionMetadata,
+    type AlbumSortOptionMetadata,
+    collapseAllAlbumGroups,
+    createAlbumAndRedirect,
+    expandAllAlbumGroups,
+    findFilterOption,
+    findGroupOptionMetadata,
+    findSortOptionMetadata,
+    getSelectedAlbumGroupOption,
+    groupOptionsMetadata,
+    sortOptionsMetadata,
+  } from '$lib/utils/album-utils';
+  import { Button, IconButton, Text } from '@immich/ui';
   import {
     mdiArrowDownThin,
     mdiArrowUpThin,
@@ -22,21 +37,8 @@
     mdiUnfoldMoreHorizontal,
     mdiViewGridOutline,
   } from '@mdi/js';
-  import {
-    type AlbumGroupOptionMetadata,
-    type AlbumSortOptionMetadata,
-    findGroupOptionMetadata,
-    findFilterOption,
-    findSortOptionMetadata,
-    getSelectedAlbumGroupOption,
-    groupOptionsMetadata,
-    sortOptionsMetadata,
-  } from '$lib/utils/album-utils';
-  import SearchBar from '$lib/components/elements/search-bar.svelte';
-  import GroupTab from '$lib/components/elements/group-tab.svelte';
-  import { createAlbumAndRedirect, collapseAllAlbumGroups, expandAllAlbumGroups } from '$lib/utils/album-utils';
-  import { fly } from 'svelte/transition';
   import { t } from 'svelte-i18n';
+  import { fly } from 'svelte/transition';
 
   interface Props {
     albumGroups: string[];
@@ -127,12 +129,10 @@
 </div>
 
 <!-- Create Album -->
-<LinkButton onclick={() => createAlbumAndRedirect()}>
-  <div class="flex place-items-center gap-2 text-sm">
-    <Icon path={mdiPlusBoxOutline} size="18" />
-    <p class="hidden md:block">{$t('create_album')}</p>
-  </div>
-</LinkButton>
+<Button onclick={() => createAlbumAndRedirect()} size="small" variant="ghost" color="secondary">
+  <Icon path={mdiPlusBoxOutline} />
+  <p class="hidden md:block">{$t('create_album')}</p>
+</Button>
 
 <!-- Sort Albums -->
 <Dropdown
@@ -164,34 +164,38 @@
     <!-- Expand Album Groups -->
     <div class="hidden xl:flex gap-0">
       <div class="block">
-        <LinkButton title={$t('expand_all')} onclick={() => expandAllAlbumGroups()}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiUnfoldMoreHorizontal} size="18" />
-          </div>
-        </LinkButton>
+        <IconButton
+          title={$t('expand_all')}
+          onclick={() => expandAllAlbumGroups()}
+          variant="ghost"
+          color="secondary"
+          shape="round"
+          icon={mdiUnfoldMoreHorizontal}
+        />
       </div>
 
       <!-- Collapse Album Groups -->
       <div class="block">
-        <LinkButton title={$t('collapse_all')} onclick={() => collapseAllAlbumGroups(albumGroups)}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiUnfoldLessHorizontal} size="18" />
-          </div>
-        </LinkButton>
+        <IconButton
+          title={$t('collapse_all')}
+          onclick={() => collapseAllAlbumGroups(albumGroups)}
+          variant="ghost"
+          color="secondary"
+          shape="round"
+          icon={mdiUnfoldLessHorizontal}
+        />
       </div>
     </div>
   </span>
 {/if}
 
 <!-- Cover/List Display Toggle -->
-<LinkButton onclick={() => handleChangeListMode()}>
-  <div class="flex place-items-center gap-2 text-sm">
-    {#if $albumViewSettings.view === AlbumViewMode.List}
-      <Icon path={mdiViewGridOutline} size="18" />
-      <p class="hidden md:block">{$t('covers')}</p>
-    {:else}
-      <Icon path={mdiFormatListBulletedSquare} size="18" />
-      <p class="hidden md:block">{$t('list')}</p>
-    {/if}
-  </div>
-</LinkButton>
+<Button onclick={() => handleChangeListMode()} size="small" variant="ghost" color="secondary">
+  {#if $albumViewSettings.view === AlbumViewMode.List}
+    <Icon path={mdiViewGridOutline} />
+    <Text class="hidden md:block">{$t('covers')}</Text>
+  {:else}
+    <Icon path={mdiFormatListBulletedSquare} size="18" />
+    <Text class="hidden md:block">{$t('list')}</Text>
+  {/if}
+</Button>
diff --git a/web/src/lib/components/elements/buttons/link-button.svelte b/web/src/lib/components/elements/buttons/link-button.svelte
deleted file mode 100644
index a39e2608cf..0000000000
--- a/web/src/lib/components/elements/buttons/link-button.svelte
+++ /dev/null
@@ -1,25 +0,0 @@
-<script lang="ts" module>
-  export type Color = 'transparent-primary' | 'transparent-gray';
-</script>
-
-<script lang="ts">
-  import Button from '$lib/components/elements/buttons/button.svelte';
-  import type { Snippet } from 'svelte';
-
-  interface Props {
-    href?: string;
-    color?: Color;
-    children?: Snippet;
-    onclick?: (e: MouseEvent) => void;
-    title?: string;
-    disabled?: boolean;
-    fullwidth?: boolean;
-    class?: string;
-  }
-
-  let { color = 'transparent-gray', children, ...rest }: Props = $props();
-</script>
-
-<Button size="link" {color} shadow={false} rounded="lg" {...rest}>
-  {@render children?.()}
-</Button>
diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte
index b146f347dc..25d279b0f4 100644
--- a/web/src/lib/components/elements/dropdown.svelte
+++ b/web/src/lib/components/elements/dropdown.svelte
@@ -11,14 +11,12 @@
 </script>
 
 <script lang="ts" generics="T">
-  import Icon from './icon.svelte';
-
-  import { mdiCheck } from '@mdi/js';
-
-  import { isEqual } from 'lodash-es';
-  import LinkButton from './buttons/link-button.svelte';
   import { clickOutside } from '$lib/actions/click-outside';
+  import { Button, Text } from '@immich/ui';
+  import { mdiCheck } from '@mdi/js';
+  import { isEqual } from 'lodash-es';
   import { fly } from 'svelte/transition';
+  import Icon from './icon.svelte';
 
   interface Props {
     class?: string;
@@ -82,14 +80,12 @@
 
 <div use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}>
   <!-- BUTTON TITLE -->
-  <LinkButton onclick={() => (showMenu = true)} fullwidth {title}>
-    <div class="flex place-items-center gap-2 text-sm">
-      {#if renderedSelectedOption?.icon}
-        <Icon path={renderedSelectedOption.icon} size="18" />
-      {/if}
-      <p class={hideTextOnSmallScreen ? 'hidden sm:block' : ''}>{renderedSelectedOption.title}</p>
-    </div>
-  </LinkButton>
+  <Button onclick={() => (showMenu = true)} fullWidth {title} variant="ghost" color="secondary" size="small">
+    {#if renderedSelectedOption?.icon}
+      <Icon path={renderedSelectedOption.icon} />
+    {/if}
+    <Text class={hideTextOnSmallScreen ? 'hidden sm:block' : ''}>{renderedSelectedOption.title}</Text>
+  </Button>
 
   <!-- DROP DOWN MENU -->
   {#if showMenu}
@@ -108,7 +104,7 @@
         >
           {#if isEqual(selectedOption, option)}
             <div class="text-immich-primary dark:text-immich-dark-primary">
-              <Icon path={mdiCheck} size="18" />
+              <Icon path={mdiCheck} />
             </div>
             <p class="justify-self-start text-immich-primary dark:text-immich-dark-primary">
               {renderedOption.title}
diff --git a/web/src/lib/components/map-page/map-settings-modal.svelte b/web/src/lib/components/map-page/map-settings-modal.svelte
index 270978e120..387da7f997 100644
--- a/web/src/lib/components/map-page/map-settings-modal.svelte
+++ b/web/src/lib/components/map-page/map-settings-modal.svelte
@@ -1,13 +1,11 @@
 <script lang="ts">
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
-  import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
   import type { MapSettings } from '$lib/stores/preferences.store';
+  import { Button, Field, Stack, Switch } from '@immich/ui';
   import { Duration } from 'luxon';
   import { t } from 'svelte-i18n';
   import { fly } from 'svelte/transition';
-  import Button from '../elements/buttons/button.svelte';
-  import LinkButton from '../elements/buttons/link-button.svelte';
   import DateInput from '../elements/date-input.svelte';
 
   interface Props {
@@ -16,7 +14,8 @@
     onSave: (settings: MapSettings) => void;
   }
 
-  let { settings = $bindable(), onClose, onSave }: Props = $props();
+  let { settings: initialValues, onClose, onSave }: Props = $props();
+  let settings = $state(initialValues);
 
   let customDateRange = $state(!!settings.dateAfter || !!settings.dateBefore);
 
@@ -26,90 +25,109 @@
   };
 </script>
 
-<FullScreenModal title={$t('map_settings')} {onClose}>
-  <form {onsubmit} class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary" id="map-settings-form">
-    <SettingSwitch title={$t('allow_dark_mode')} bind:checked={settings.allowDarkMode} />
-    <SettingSwitch title={$t('only_favorites')} bind:checked={settings.onlyFavorites} />
-    <SettingSwitch title={$t('include_archived')} bind:checked={settings.includeArchived} />
-    <SettingSwitch title={$t('include_shared_partner_assets')} bind:checked={settings.withPartners} />
-    <SettingSwitch title={$t('include_shared_albums')} bind:checked={settings.withSharedAlbums} />
-    {#if customDateRange}
-      <div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
-        <div class="flex items-center justify-between gap-8">
-          <label class="immich-form-label shrink-0 text-sm" for="date-after">{$t('date_after')}</label>
-          <DateInput
-            class="immich-form-input w-40"
-            type="date"
-            id="date-after"
-            max={settings.dateBefore}
-            bind:value={settings.dateAfter}
-          />
-        </div>
-        <div class="flex items-center justify-between gap-8">
-          <label class="immich-form-label shrink-0 text-sm" for="date-before">{$t('date_before')}</label>
-          <DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
-        </div>
-        <div class="flex justify-center text-xs">
-          <LinkButton
-            onclick={() => {
-              customDateRange = false;
-              settings.dateAfter = '';
-              settings.dateBefore = '';
-            }}
-          >
-            {$t('remove_custom_date_range')}
-          </LinkButton>
-        </div>
-      </div>
-    {:else}
-      <div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
-        <SettingSelect
-          label={$t('date_range')}
-          name="date-range"
-          bind:value={settings.relativeDate}
-          options={[
-            {
-              value: '',
-              text: $t('all'),
-            },
-            {
-              value: Duration.fromObject({ hours: 24 }).toISO() || '',
-              text: $t('past_durations.hours', { values: { hours: 24 } }),
-            },
-            {
-              value: Duration.fromObject({ days: 7 }).toISO() || '',
-              text: $t('past_durations.days', { values: { days: 7 } }),
-            },
-            {
-              value: Duration.fromObject({ days: 30 }).toISO() || '',
-              text: $t('past_durations.days', { values: { days: 30 } }),
-            },
-            {
-              value: Duration.fromObject({ years: 1 }).toISO() || '',
-              text: $t('past_durations.years', { values: { years: 1 } }),
-            },
-            {
-              value: Duration.fromObject({ years: 3 }).toISO() || '',
-              text: $t('past_durations.years', { values: { years: 3 } }),
-            },
-          ]}
-        />
-        <div class="text-xs">
-          <LinkButton
-            onclick={() => {
-              customDateRange = true;
-              settings.relativeDate = '';
-            }}
-          >
-            {$t('use_custom_date_range')}
-          </LinkButton>
-        </div>
-      </div>
-    {/if}
-  </form>
+<form {onsubmit}>
+  <FullScreenModal title={$t('map_settings')} {onClose}>
+    <Stack gap={4}>
+      <Field label={$t('allow_dark_mode')}>
+        <Switch bind:checked={settings.allowDarkMode} class="flex justify-between items-center text-sm" />
+      </Field>
+      <Field label={$t('only_favorites')}>
+        <Switch bind:checked={settings.onlyFavorites} class="flex justify-between items-center text-sm" />
+      </Field>
+      <Field label={$t('include_archived')}>
+        <Switch bind:checked={settings.includeArchived} class="flex justify-between items-center text-sm" />
+      </Field>
+      <Field label={$t('include_shared_partner_assets')}>
+        <Switch bind:checked={settings.withPartners} class="flex justify-between items-center text-sm" />
+      </Field>
+      <Field label={$t('include_shared_albums')}>
+        <Switch bind:checked={settings.withSharedAlbums} class="flex justify-between items-center text-sm" />
+      </Field>
 
-  {#snippet stickyBottom()}
-    <Button color="gray" size="sm" fullwidth onclick={onClose}>{$t('cancel')}</Button>
-    <Button type="submit" size="sm" fullwidth form="map-settings-form">{$t('save')}</Button>
-  {/snippet}
-</FullScreenModal>
+      {#if customDateRange}
+        <div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
+          <div class="flex items-center justify-between gap-8">
+            <label class="immich-form-label shrink-0 text-sm" for="date-after">{$t('date_after')}</label>
+            <DateInput
+              class="immich-form-input w-40"
+              type="date"
+              id="date-after"
+              max={settings.dateBefore}
+              bind:value={settings.dateAfter}
+            />
+          </div>
+          <div class="flex items-center justify-between gap-8">
+            <label class="immich-form-label shrink-0 text-sm" for="date-before">{$t('date_before')}</label>
+            <DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
+          </div>
+          <div class="flex justify-center text-xs">
+            <Button
+              color="primary"
+              size="small"
+              variant="ghost"
+              onclick={() => {
+                customDateRange = false;
+                settings.dateAfter = '';
+                settings.dateBefore = '';
+              }}
+            >
+              {$t('remove_custom_date_range')}
+            </Button>
+          </div>
+        </div>
+      {:else}
+        <div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
+          <SettingSelect
+            label={$t('date_range')}
+            name="date-range"
+            bind:value={settings.relativeDate}
+            options={[
+              {
+                value: '',
+                text: $t('all'),
+              },
+              {
+                value: Duration.fromObject({ hours: 24 }).toISO() || '',
+                text: $t('past_durations.hours', { values: { hours: 24 } }),
+              },
+              {
+                value: Duration.fromObject({ days: 7 }).toISO() || '',
+                text: $t('past_durations.days', { values: { days: 7 } }),
+              },
+              {
+                value: Duration.fromObject({ days: 30 }).toISO() || '',
+                text: $t('past_durations.days', { values: { days: 30 } }),
+              },
+              {
+                value: Duration.fromObject({ years: 1 }).toISO() || '',
+                text: $t('past_durations.years', { values: { years: 1 } }),
+              },
+              {
+                value: Duration.fromObject({ years: 3 }).toISO() || '',
+                text: $t('past_durations.years', { values: { years: 3 } }),
+              },
+            ]}
+          />
+          <div class="text-xs">
+            <Button
+              color="primary"
+              size="small"
+              variant="ghost"
+              onclick={() => {
+                customDateRange = true;
+                settings.relativeDate = '';
+              }}
+            >
+              {$t('use_custom_date_range')}
+            </Button>
+          </div>
+        </div>
+      {/if}
+    </Stack>
+
+    {#snippet stickyBottom()}
+      <Button color="secondary" shape="round" fullWidth onclick={onClose}>{$t('cancel')}</Button>
+      <Button type="submit" shape="round" fullWidth>{$t('save')}</Button>
+    {/snippet}
+  </FullScreenModal>
+</form>
diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
index 443e8f06b1..77890529b7 100644
--- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
+++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
@@ -1,21 +1,20 @@
 <script lang="ts">
   import Button from '$lib/components/elements/buttons/button.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
-  import Icon from '$lib/components/elements/icon.svelte';
+  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
+  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
+  import { SettingInputFieldType } from '$lib/constants';
+  import { locale } from '$lib/stores/preferences.store';
   import { serverConfig } from '$lib/stores/server-config.store';
   import { copyToClipboard, makeSharedLinkUrl } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
   import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
+  import { HStack, IconButton, Input } from '@immich/ui';
   import { mdiContentCopy, mdiLink } from '@mdi/js';
+  import { DateTime, Duration } from 'luxon';
+  import { t } from 'svelte-i18n';
   import { NotificationType, notificationController } from '../notification/notification';
   import SettingInputField from '../settings/setting-input-field.svelte';
   import SettingSwitch from '../settings/setting-switch.svelte';
-  import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
-  import { t } from 'svelte-i18n';
-  import { locale } from '$lib/stores/preferences.store';
-  import { DateTime, Duration } from 'luxon';
-  import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
-  import { SettingInputFieldType } from '$lib/constants';
 
   interface Props {
     onClose: () => void;
@@ -243,14 +242,18 @@
         <Button size="sm" fullwidth onclick={handleCreateSharedLink}>{$t('create_link')}</Button>
       {/if}
     {:else}
-      <div class="flex w-full gap-2">
-        <input class="immich-form-input w-full" bind:value={sharedLink} disabled />
-        <LinkButton onclick={() => (sharedLink ? copyToClipboard(sharedLink) : '')}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiContentCopy} ariaLabel={$t('copy_link_to_clipboard')} size="18" />
-          </div>
-        </LinkButton>
-      </div>
+      <HStack class="w-full">
+        <Input bind:value={sharedLink} disabled class="flex flex-row" />
+        <IconButton
+          variant="ghost"
+          shape="round"
+          color="secondary"
+          size="giant"
+          icon={mdiContentCopy}
+          onclick={() => (sharedLink ? copyToClipboard(sharedLink) : '')}
+          aria-label={$t('copy_link_to_clipboard')}
+        />
+      </HStack>
     {/if}
   {/snippet}
 </FullScreenModal>
diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
index ae63a249b5..193cc2ed43 100644
--- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
+++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte
@@ -2,25 +2,25 @@
   import { page } from '$app/state';
   import { clickOutside } from '$lib/actions/click-outside';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
+  import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
+  import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
+  import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
+  import { AppRoute } from '$lib/constants';
   import { featureFlags } from '$lib/stores/server-config.store';
   import { user } from '$lib/stores/user.store';
   import { userInteraction } from '$lib/stores/user.svelte';
   import { handleLogout } from '$lib/utils/auth';
   import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
+  import { Button, IconButton } from '@immich/ui';
   import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
+  import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
   import { fade } from 'svelte/transition';
-  import { AppRoute } from '$lib/constants';
-  import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
-  import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
   import ThemeButton from '../theme-button.svelte';
   import UserAvatar from '../user-avatar.svelte';
   import AccountInfoPanel from './account-info-panel.svelte';
-  import HelpAndFeedbackModal from '$lib/components/shared-components/help-and-feedback-modal.svelte';
-  import { onMount } from 'svelte';
 
   interface Props {
     showUploadButton?: boolean;
@@ -87,22 +87,24 @@
             onEscape: () => (shouldShowHelpPanel = false),
           }}
         >
-          <CircleIconButton
-            id="support-feedback-button"
+          <IconButton
+            shape="round"
+            color="secondary"
+            variant="ghost"
+            size="large"
             title={$t('support_and_feedback')}
             icon={mdiHelpCircleOutline}
             onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
-            padding="1"
           />
         </div>
 
         {#if !page.url.pathname.includes('/admin') && showUploadButton}
-          <LinkButton onclick={onUploadClick} class="hidden lg:block">
+          <Button onclick={onUploadClick} class="hidden lg:block" variant="ghost" color="secondary">
             <div class="flex gap-2">
               <Icon path={mdiTrayArrowUp} size="1.5em" />
               <span>{$t('upload')}</span>
             </div>
-          </LinkButton>
+          </Button>
           <CircleIconButton
             onclick={onUploadClick}
             title={$t('upload')}
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte
index 0b51a7e240..c367010353 100644
--- a/web/src/routes/(user)/people/+page.svelte
+++ b/web/src/routes/(user)/people/+page.svelte
@@ -3,8 +3,6 @@
   import { page } from '$app/stores';
   import { focusTrap } from '$lib/actions/focus-trap';
   import { scrollMemory } from '$lib/actions/scroll-memory';
-  import Button from '$lib/components/elements/buttons/button.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
   import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
@@ -32,6 +30,7 @@
     updatePerson,
     type PersonResponseDto,
   } from '@immich/sdk';
+  import { Button, Text } from '@immich/ui';
   import { mdiAccountOff, mdiEyeOutline } from '@mdi/js';
   import { onMount } from 'svelte';
   import { t } from 'svelte-i18n';
@@ -393,12 +392,10 @@
             />
           </div>
         </div>
-        <LinkButton onclick={() => (selectHidden = !selectHidden)}>
-          <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-            <Icon path={mdiEyeOutline} size="18" />
-            <p class="ml-2">{$t('show_and_hide_people')}</p>
-          </div>
-        </LinkButton>
+        <Button onclick={() => (selectHidden = !selectHidden)} size="small" variant="ghost" color="secondary">
+          <Icon path={mdiEyeOutline} />
+          <Text>{$t('show_and_hide_people')}</Text>
+        </Button>
       </div>
     {/if}
   {/snippet}
@@ -445,13 +442,13 @@
 
       {#snippet stickyBottom()}
         <Button
-          color="gray"
-          fullwidth
+          color="secondary"
+          fullWidth
           onclick={() => {
             showChangeNameModal = false;
           }}>{$t('cancel')}</Button
         >
-        <Button type="submit" fullwidth form="change-name-form">{$t('ok')}</Button>
+        <Button type="submit" fullWidth form="change-name-form">{$t('ok')}</Button>
       {/snippet}
     </FullScreenModal>
   {/if}
diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte
index 1e59a2720d..33282761aa 100644
--- a/web/src/routes/(user)/sharing/+page.svelte
+++ b/web/src/routes/(user)/sharing/+page.svelte
@@ -1,14 +1,11 @@
 <script lang="ts">
   import empty2Url from '$lib/assets/empty-2.svg';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
+  import Albums from '$lib/components/album-page/albums-list.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
   import { AppRoute } from '$lib/constants';
-  import { mdiLink, mdiPlusBoxOutline } from '@mdi/js';
-  import type { PageData } from './$types';
-  import { createAlbumAndRedirect } from '$lib/utils/album-utils';
   import {
     AlbumFilter,
     AlbumGroupBy,
@@ -17,8 +14,11 @@
     SortOrder,
     type AlbumViewSettings,
   } from '$lib/stores/preferences.store';
-  import Albums from '$lib/components/album-page/albums-list.svelte';
+  import { createAlbumAndRedirect } from '$lib/utils/album-utils';
+  import { Button, HStack } from '@immich/ui';
+  import { mdiLink, mdiPlusBoxOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -39,21 +39,17 @@
 
 <UserPageLayout title={data.meta.title}>
   {#snippet buttons()}
-    <div class="flex">
-      <LinkButton onclick={() => createAlbumAndRedirect()}>
-        <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-          <Icon path={mdiPlusBoxOutline} size="18" class="shrink-0" />
-          <span class="leading-none max-sm:text-xs">{$t('create_album')}</span>
-        </div>
-      </LinkButton>
+    <HStack gap={0}>
+      <Button onclick={() => createAlbumAndRedirect()} size="small" variant="ghost" color="secondary">
+        <Icon path={mdiPlusBoxOutline} class="shrink-0" />
+        <span class="leading-none max-sm:text-xs">{$t('create_album')}</span>
+      </Button>
 
-      <LinkButton href={AppRoute.SHARED_LINKS}>
-        <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
-          <Icon path={mdiLink} size="18" class="shrink-0" />
-          <span class="leading-none max-sm:text-xs">{$t('shared_links')}</span>
-        </div>
-      </LinkButton>
-    </div>
+      <Button href={AppRoute.SHARED_LINKS} size="small" variant="ghost" color="secondary">
+        <Icon path={mdiLink} class="shrink-0" />
+        <span class="leading-none max-sm:text-xs">{$t('shared_links')}</span>
+      </Button>
+    </HStack>
   {/snippet}
 
   <div class="flex flex-col">
diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
index b9955644a8..742e386722 100644
--- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,11 +1,11 @@
 <script lang="ts">
   import { goto } from '$app/navigation';
   import { page } from '$app/stores';
-  import Button from '$lib/components/elements/buttons/button.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
+  import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
+  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
   import {
     notificationController,
@@ -13,19 +13,18 @@
   } from '$lib/components/shared-components/notification/notification';
   import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
   import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
+  import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
   import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
   import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
   import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { AssetStore } from '$lib/stores/assets.store';
   import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
   import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
+  import { Button, HStack, Text } from '@immich/ui';
   import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
-  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
-  import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
-  import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
-  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
 
   interface Props {
     data: PageData;
@@ -169,29 +168,23 @@
   {/snippet}
 
   {#snippet buttons()}
-    <section>
-      <LinkButton onclick={handleCreate}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiPlus} size="18" />
-          <p class="hidden md:block">{$t('create_tag')}</p>
-        </div>
-      </LinkButton>
+    <HStack>
+      <Button onclick={handleCreate} size="small" variant="ghost" color="secondary">
+        <Icon path={mdiPlus} />
+        <p class="hidden md:block">{$t('create_tag')}</p>
+      </Button>
 
       {#if pathSegments.length > 0 && tag}
-        <LinkButton onclick={handleEdit}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiPencil} size="18" />
-            <p class="hidden md:block">{$t('edit_tag')}</p>
-          </div>
-        </LinkButton>
-        <LinkButton onclick={handleDelete}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiTrashCanOutline} size="18" />
-            <p class="hidden md:block">{$t('delete_tag')}</p>
-          </div>
-        </LinkButton>
+        <Button onclick={handleEdit} size="small" variant="ghost" color="secondary">
+          <Icon path={mdiPencil} size="18" />
+          <Text class="hidden md:block">{$t('edit_tag')}</Text>
+        </Button>
+        <Button onclick={handleDelete} size="small" variant="ghost" color="secondary">
+          <Icon path={mdiTrashCanOutline} />
+          <Text class="hidden md:block">{$t('delete_tag')}</Text>
+        </Button>
       {/if}
-    </section>
+    </HStack>
   {/snippet}
 
   <Breadcrumbs {pathSegments} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
@@ -230,8 +223,8 @@
     </form>
 
     {#snippet stickyBottom()}
-      <Button color="gray" fullwidth onclick={() => handleCancel()}>{$t('cancel')}</Button>
-      <Button type="submit" fullwidth form="create-tag-form">{$t('create')}</Button>
+      <Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button>
+      <Button type="submit" fullWidth shape="round" form="create-tag-form">{$t('create')}</Button>
     {/snippet}
   </FullScreenModal>
 {/if}
@@ -249,8 +242,8 @@
     </form>
 
     {#snippet stickyBottom()}
-      <Button color="gray" fullwidth onclick={() => handleCancel()}>{$t('cancel')}</Button>
-      <Button type="submit" fullwidth form="edit-tag-form">{$t('save')}</Button>
+      <Button color="secondary" fullWidth shape="round" onclick={() => handleCancel()}>{$t('cancel')}</Button>
+      <Button type="submit" fullWidth shape="round" form="edit-tag-form">{$t('save')}</Button>
     {/snippet}
   </FullScreenModal>
 {/if}
diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 686d82b125..031d2a6268 100644
--- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,7 +1,6 @@
 <script lang="ts">
   import { goto } from '$app/navigation';
   import empty3Url from '$lib/assets/empty-3.svg';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
@@ -9,23 +8,24 @@
   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
+  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import {
     NotificationType,
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { AppRoute } from '$lib/constants';
+  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
   import { AssetStore } from '$lib/stores/assets.store';
   import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
+  import { handlePromiseError } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
   import { emptyTrash, restoreTrash } from '@immich/sdk';
+  import { Button, HStack } from '@immich/ui';
   import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js';
-  import type { PageData } from './$types';
-  import { handlePromiseError } from '$lib/utils';
-  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
-  import { t } from 'svelte-i18n';
   import { onDestroy } from 'svelte';
-  import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
+  import { t } from 'svelte-i18n';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -113,20 +113,28 @@
 {#if $featureFlags.loaded && $featureFlags.trash}
   <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
     {#snippet buttons()}
-      <div class="flex place-items-center gap-2">
-        <LinkButton onclick={handleRestoreTrash} disabled={assetInteraction.selectionActive}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiHistory} size="18" />
-            {$t('restore_all')}
-          </div>
-        </LinkButton>
-        <LinkButton onclick={() => handleEmptyTrash()} disabled={assetInteraction.selectionActive}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiDeleteForeverOutline} size="18" />
-            {$t('empty_trash')}
-          </div>
-        </LinkButton>
-      </div>
+      <HStack gap={0}>
+        <Button
+          onclick={handleRestoreTrash}
+          disabled={assetInteraction.selectionActive}
+          variant="ghost"
+          color="secondary"
+          size="small"
+        >
+          <Icon path={mdiHistory} />
+          {$t('restore_all')}
+        </Button>
+        <Button
+          onclick={() => handleEmptyTrash()}
+          disabled={assetInteraction.selectionActive}
+          variant="ghost"
+          color="secondary"
+          size="small"
+        >
+          <Icon path={mdiDeleteForeverOutline} />
+          {$t('empty_trash')}
+        </Button>
+      </HStack>
     {/snippet}
 
     <AssetGrid enableRouting={true} {assetStore} {assetInteraction} onEscape={handleEscape}>
diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte
index 22e4f86c74..8515d163ab 100644
--- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte
+++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte
@@ -1,27 +1,26 @@
 <script lang="ts">
+  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
+  import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import { dialogController } from '$lib/components/shared-components/dialog/dialog';
+  import DuplicatesModal from '$lib/components/shared-components/duplicates-modal.svelte';
   import {
     NotificationType,
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
+  import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte';
   import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte';
-  import type { AssetResponseDto } from '@immich/sdk';
+  import { locale } from '$lib/stores/preferences.store';
   import { featureFlags } from '$lib/stores/server-config.store';
+  import { stackAssets } from '$lib/utils/asset-utils';
+  import { suggestDuplicate } from '$lib/utils/duplicate-utils';
   import { handleError } from '$lib/utils/handle-error';
+  import type { AssetResponseDto } from '@immich/sdk';
   import { deleteAssets, updateAssets } from '@immich/sdk';
+  import { Button, HStack, IconButton } from '@immich/ui';
+  import { mdiCheckOutline, mdiInformationOutline, mdiKeyboard, mdiTrashCanOutline } from '@mdi/js';
   import { t } from 'svelte-i18n';
   import type { PageData } from './$types';
-  import { suggestDuplicate } from '$lib/utils/duplicate-utils';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
-  import { mdiCheckOutline, mdiInformationOutline, mdiTrashCanOutline } from '@mdi/js';
-  import { stackAssets } from '$lib/utils/asset-utils';
-  import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte';
-  import DuplicatesModal from '$lib/components/shared-components/duplicates-modal.svelte';
-  import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import { mdiKeyboard } from '@mdi/js';
-  import Icon from '$lib/components/elements/icon.svelte';
-  import { locale } from '$lib/stores/preferences.store';
 
   interface Props {
     data: PageData;
@@ -163,25 +162,31 @@
 
 <UserPageLayout title={data.meta.title + ` (${duplicates.length.toLocaleString($locale)})`} scrollbar={true}>
   {#snippet buttons()}
-    <div class="flex place-items-center gap-2">
-      <LinkButton onclick={() => handleDeduplicateAll()} disabled={!hasDuplicates}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiTrashCanOutline} size="18" />
-          {$t('deduplicate_all')}
-        </div>
-      </LinkButton>
-      <LinkButton onclick={() => handleKeepAll()} disabled={!hasDuplicates}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiCheckOutline} size="18" />
-          {$t('keep_all')}
-        </div>
-      </LinkButton>
-      <CircleIconButton
+    <HStack gap={0}>
+      <Button
+        onclick={() => handleDeduplicateAll()}
+        disabled={!hasDuplicates}
+        size="small"
+        variant="ghost"
+        color="secondary"
+      >
+        <Icon path={mdiTrashCanOutline} />
+        {$t('deduplicate_all')}
+      </Button>
+      <Button onclick={() => handleKeepAll()} disabled={!hasDuplicates} size="small" variant="ghost" color="secondary">
+        <Icon path={mdiCheckOutline} />
+        {$t('keep_all')}
+      </Button>
+      <IconButton
+        shape="round"
+        variant="ghost"
+        color="secondary"
+        size="large"
         icon={mdiKeyboard}
         title={$t('show_keyboard_shortcuts')}
         onclick={() => (isShowKeyboardShortcut = !isShowKeyboardShortcut)}
       />
-    </div>
+    </HStack>
   {/snippet}
 
   <div class="">
diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte
index b323a136aa..e3657f805a 100644
--- a/web/src/routes/admin/jobs-status/+page.svelte
+++ b/web/src/routes/admin/jobs-status/+page.svelte
@@ -1,6 +1,5 @@
 <script lang="ts">
   import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
@@ -13,6 +12,7 @@
   import { asyncTimeout } from '$lib/utils';
   import { handleError } from '$lib/utils/handle-error';
   import { createJob, getAllJobsStatus, ManualJobName, type AllJobStatusResponseDto } from '@immich/sdk';
+  import { Button, HStack } from '@immich/ui';
   import { mdiCog, mdiPlus } from '@mdi/js';
   import { onDestroy, onMount } from 'svelte';
   import { t } from 'svelte-i18n';
@@ -71,20 +71,16 @@
 
 <UserPageLayout title={data.meta.title} admin>
   {#snippet buttons()}
-    <div class="flex justify-end">
-      <LinkButton onclick={() => (isOpen = true)}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiPlus} size="18" />
-          {$t('admin.create_job')}
-        </div>
-      </LinkButton>
-      <LinkButton href="{AppRoute.ADMIN_SETTINGS}?isOpen=job">
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiCog} size="18" />
-          {$t('admin.manage_concurrency')}
-        </div>
-      </LinkButton>
-    </div>
+    <HStack gap={0}>
+      <Button onclick={() => (isOpen = true)} size="small" variant="ghost" color="secondary">
+        <Icon path={mdiPlus} size="18" />
+        {$t('admin.create_job')}
+      </Button>
+      <Button href="{AppRoute.ADMIN_SETTINGS}?isOpen=job" size="small" variant="ghost" color="secondary">
+        <Icon path={mdiCog} size="18" />
+        {$t('admin.manage_concurrency')}
+      </Button>
+    </HStack>
   {/snippet}
   <section id="setting-content" class="flex place-content-center sm:mx-4">
     <section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
index b89e81ebf6..eb3f22878e 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -1,17 +1,20 @@
 <script lang="ts">
   import Icon from '$lib/components/elements/icon.svelte';
-  import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import LibraryImportPathsForm from '$lib/components/forms/library-import-paths-form.svelte';
   import LibraryRenameForm from '$lib/components/forms/library-rename-form.svelte';
   import LibraryScanSettingsForm from '$lib/components/forms/library-scan-settings-form.svelte';
   import LibraryUserPickerForm from '$lib/components/forms/library-user-picker-form.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.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 { dialogController } from '$lib/components/shared-components/dialog/dialog';
+  import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
   import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
   import {
     notificationController,
     NotificationType,
   } from '$lib/components/shared-components/notification/notification';
+  import { locale } from '$lib/stores/preferences.store';
   import { ByteUnit, getBytesWithUnit } from '$lib/utils/byte-units';
   import { handleError } from '$lib/utils/handle-error';
   import {
@@ -26,15 +29,12 @@
     type LibraryStatsResponseDto,
     type UserResponseDto,
   } from '@immich/sdk';
+  import { Button } from '@immich/ui';
   import { mdiDatabase, mdiDotsVertical, mdiPlusBoxOutline, mdiSync } from '@mdi/js';
   import { onMount } from 'svelte';
-  import { fade, slide } from 'svelte/transition';
-  import LinkButton from '../../../lib/components/elements/buttons/link-button.svelte';
-  import type { PageData } from './$types';
-  import { dialogController } from '$lib/components/shared-components/dialog/dialog';
   import { t } from 'svelte-i18n';
-  import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
-  import { locale } from '$lib/stores/preferences.store';
+  import { fade, slide } from 'svelte/transition';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -220,19 +220,19 @@
   {#snippet buttons()}
     <div class="flex justify-end gap-2">
       {#if libraries.length > 0}
-        <LinkButton onclick={() => handleScanAll()}>
+        <Button onclick={handleScanAll} size="small" variant="ghost" color="secondary">
           <div class="flex gap-1 text-sm">
             <Icon path={mdiSync} size="18" />
             <span>{$t('scan_all_libraries')}</span>
           </div>
-        </LinkButton>
+        </Button>
       {/if}
-      <LinkButton onclick={() => (toCreateLibrary = true)}>
+      <Button onclick={() => (toCreateLibrary = true)} size="small" variant="ghost" color="secondary">
         <div class="flex gap-1 text-sm">
           <Icon path={mdiPlusBoxOutline} size="18" />
           <span>{$t('create_library')}</span>
         </div>
-      </LinkButton>
+      </Button>
     </div>
   {/snippet}
   <section class="my-4">
diff --git a/web/src/routes/admin/repair/+page.svelte b/web/src/routes/admin/repair/+page.svelte
index 9f19fddd03..a6ffd52322 100644
--- a/web/src/routes/admin/repair/+page.svelte
+++ b/web/src/routes/admin/repair/+page.svelte
@@ -1,7 +1,6 @@
 <script lang="ts">
   import empty4Url from '$lib/assets/empty-4.svg';
   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
@@ -10,14 +9,15 @@
     notificationController,
   } from '$lib/components/shared-components/notification/notification';
   import { downloadManager } from '$lib/stores/download';
+  import { locale } from '$lib/stores/preferences.store';
   import { copyToClipboard } from '$lib/utils';
   import { downloadBlob } from '$lib/utils/asset-utils';
   import { handleError } from '$lib/utils/handle-error';
   import { fixAuditFiles, getAuditFiles, getFileChecksums, type FileReportItemDto } from '@immich/sdk';
+  import { Button, HStack } from '@immich/ui';
   import { mdiCheckAll, mdiContentCopy, mdiDownload, mdiRefresh, mdiWrench } from '@mdi/js';
-  import type { PageData } from './$types';
   import { t } from 'svelte-i18n';
-  import { locale } from '$lib/stores/preferences.store';
+  import type { PageData } from './$types';
 
   interface Props {
     data: PageData;
@@ -185,32 +185,42 @@
 
 <UserPageLayout title={data.meta.title} admin>
   {#snippet buttons()}
-    <div class="flex justify-end gap-2">
-      <LinkButton onclick={() => handleRepair()} disabled={matches.length === 0 || repairing}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiWrench} size="18" />
-          {$t('admin.repair_all')}
-        </div>
-      </LinkButton>
-      <LinkButton onclick={() => handleCheckAll()} disabled={extras.length === 0 || checking}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiCheckAll} size="18" />
-          {$t('admin.check_all')}
-        </div>
-      </LinkButton>
-      <LinkButton onclick={() => handleDownload()} disabled={extras.length + orphans.length === 0}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiDownload} size="18" />
-          {$t('export')}
-        </div>
-      </LinkButton>
-      <LinkButton onclick={() => handleRefresh()}>
-        <div class="flex place-items-center gap-2 text-sm">
-          <Icon path={mdiRefresh} size="18" />
-          {$t('refresh')}
-        </div>
-      </LinkButton>
-    </div>
+    <HStack gap={0}>
+      <Button
+        onclick={() => handleRepair()}
+        disabled={matches.length === 0 || repairing}
+        size="small"
+        variant="ghost"
+        color="secondary"
+      >
+        <Icon path={mdiWrench} />
+        {$t('admin.repair_all')}
+      </Button>
+      <Button
+        onclick={() => handleCheckAll()}
+        disabled={extras.length === 0 || checking}
+        size="small"
+        variant="ghost"
+        color="secondary"
+      >
+        <Icon path={mdiCheckAll} />
+        {$t('admin.check_all')}
+      </Button>
+      <Button
+        onclick={() => handleDownload()}
+        disabled={extras.length + orphans.length === 0}
+        size="small"
+        variant="ghost"
+        color="secondary"
+      >
+        <Icon path={mdiDownload} />
+        {$t('export')}
+      </Button>
+      <Button onclick={() => handleRefresh()} size="small" variant="ghost" color="secondary">
+        <Icon path={mdiRefresh} />
+        {$t('refresh')}
+      </Button>
+    </HStack>
   {/snippet}
   <section id="setting-content" class="flex place-content-center sm:mx-4">
     <section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte
index 6a71245051..c54aeba631 100644
--- a/web/src/routes/admin/system-settings/+page.svelte
+++ b/web/src/routes/admin/system-settings/+page.svelte
@@ -17,7 +17,7 @@
   import ThemeSettings from '$lib/components/admin-page/settings/theme/theme-settings.svelte';
   import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte';
   import UserSettings from '$lib/components/admin-page/settings/user-settings/user-settings.svelte';
-  import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
+  import { Button, HStack } from '@immich/ui';
   import Icon from '$lib/components/elements/icon.svelte';
   import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
   import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
@@ -255,31 +255,30 @@
 
   <UserPageLayout title={data.meta.title} admin>
     {#snippet buttons()}
-      <div class="flex justify-end gap-2">
+      <HStack gap={1}>
         <div class="hidden lg:block">
           <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
         </div>
-        <LinkButton onclick={() => copyToClipboard(JSON.stringify(config, jsonReplacer, 2))}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiContentCopy} size="18" />
-            {$t('copy_to_clipboard')}
-          </div>
-        </LinkButton>
-        <LinkButton onclick={() => downloadConfig()}>
-          <div class="flex place-items-center gap-2 text-sm">
-            <Icon path={mdiDownload} size="18" />
-            {$t('export_as_json')}
-          </div>
-        </LinkButton>
+        <Button
+          onclick={() => copyToClipboard(JSON.stringify(config, jsonReplacer, 2))}
+          size="small"
+          variant="ghost"
+          color="secondary"
+        >
+          <Icon path={mdiContentCopy} />
+          {$t('copy_to_clipboard')}
+        </Button>
+        <Button onclick={() => downloadConfig()} size="small" variant="ghost" color="secondary">
+          <Icon path={mdiDownload} />
+          {$t('export_as_json')}
+        </Button>
         {#if !$featureFlags.configFile}
-          <LinkButton onclick={() => inputElement?.click()}>
-            <div class="flex place-items-center gap-2 text-sm">
-              <Icon path={mdiUpload} size="18" />
-              {$t('import_from_json')}
-            </div>
-          </LinkButton>
+          <Button onclick={() => inputElement?.click()} size="small" variant="ghost" color="secondary">
+            <Icon path={mdiUpload} />
+            {$t('import_from_json')}
+          </Button>
         {/if}
-      </div>
+      </HStack>
     {/snippet}
 
     <AdminSettings bind:config bind:this={adminSettingElement}>