chore: lower case text + facelift (#25263)

* chore: lower case text

* wip

* wip

* pr feedback

* pr feedback
This commit is contained in:
Alex 2026-01-21 10:41:09 -06:00 committed by GitHub
parent 0f6606848e
commit b669714bda
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 322 additions and 325 deletions

View file

@ -1555,7 +1555,7 @@
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.", "no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
"no_albums_yet": "It looks like you do not have any albums yet.", "no_albums_yet": "It looks like you do not have any albums yet.",
"no_archived_assets_message": "Archive photos and videos to hide them from your Photos view", "no_archived_assets_message": "Archive photos and videos to hide them from your Photos view",
"no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", "no_assets_message": "Click to upload your first photo",
"no_assets_to_show": "No assets to show", "no_assets_to_show": "No assets to show",
"no_cast_devices_found": "No cast devices found", "no_cast_devices_found": "No cast devices found",
"no_checksum_local": "No checksum available - cannot fetch local assets", "no_checksum_local": "No checksum available - cannot fetch local assets",
@ -2189,6 +2189,7 @@
"theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load",
"theme_setting_three_stage_loading_title": "Enable three-stage loading", "theme_setting_three_stage_loading_title": "Enable three-stage loading",
"then": "Then",
"they_will_be_merged_together": "They will be merged together", "they_will_be_merged_together": "They will be merged together",
"third_party_resources": "Third-Party Resources", "third_party_resources": "Third-Party Resources",
"time": "Time", "time": "Time",

View file

@ -4,7 +4,7 @@
/* @import '/usr/ui/dist/theme/default.css'; */ /* @import '/usr/ui/dist/theme/default.css'; */
@utility immich-form-input { @utility immich-form-input {
@apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-100 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; @apply bg-gray-100 ring-1 ring-gray-200 transition outline-none focus-within:ring-1 disabled:cursor-not-allowed dark:bg-gray-800 dark:ring-neutral-900 flex w-full items-center rounded-lg disabled:bg-gray-300 disabled:text-dark dark:disabled:bg-gray-900 dark:disabled:text-gray-200 flex-1 py-2.5 text-base pl-4 pr-4;
} }
@utility immich-form-label { @utility immich-form-label {

View file

@ -5,6 +5,7 @@
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { asQueueItem } from '$lib/services/queue.service'; import { asQueueItem } from '$lib/services/queue.service';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { transformToTitleCase } from '$lib/utils';
import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk'; import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk';
import { Icon, IconButton, Link } from '@immich/ui'; import { Icon, IconButton, Link } from '@immich/ui';
import { import {
@ -53,7 +54,7 @@
<div class="flex items-center gap-2 text-xl font-semibold text-primary"> <div class="flex items-center gap-2 text-xl font-semibold text-primary">
<Link class="flex items-center gap-2 hover:underline" href={Route.viewQueue(queue)} underline={false}> <Link class="flex items-center gap-2 hover:underline" href={Route.viewQueue(queue)} underline={false}>
<Icon {icon} size="1.25em" class="hidden shrink-0 sm:block" /> <Icon {icon} size="1.25em" class="hidden shrink-0 sm:block" />
<span class="uppercase">{title}</span> <span>{transformToTitleCase(title)}</span>
</Link> </Link>
<IconButton <IconButton
color="primary" color="primary"
@ -131,7 +132,7 @@
onClick={() => onCommand({ command: QueueCommand.Start, force: false })} onClick={() => onCommand({ command: QueueCommand.Start, force: false })}
> >
<Icon icon={mdiAlertCircle} size="36" /> <Icon icon={mdiAlertCircle} size="36" />
<span class="uppercase">{$t('disabled')}</span> <span>{$t('disabled')}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
@ -139,7 +140,7 @@
{#if waitingCount > 0} {#if waitingCount > 0}
<QueueCardButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}> <QueueCardButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}>
<Icon icon={mdiClose} size="24" /> <Icon icon={mdiClose} size="24" />
<span class="uppercase">{$t('clear')}</span> <span>{$t('clear')}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
{#if queue.isPaused} {#if queue.isPaused}
@ -147,12 +148,12 @@
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}> <QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
<!-- size property is not reactive, so have to use width and height --> <!-- size property is not reactive, so have to use width and height -->
<Icon icon={mdiFastForward} {size} /> <Icon icon={mdiFastForward} {size} />
<span class="uppercase">{$t('resume')}</span> <span>{$t('resume')}</span>
</QueueCardButton> </QueueCardButton>
{:else} {:else}
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}> <QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}>
<Icon icon={mdiPause} size="24" /> <Icon icon={mdiPause} size="24" />
<span class="uppercase">{$t('pause')}</span> <span>{$t('pause')}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
{/if} {/if}
@ -161,25 +162,25 @@
{#if allText} {#if allText}
<QueueCardButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}> <QueueCardButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}>
<Icon icon={mdiAllInclusive} size="24" /> <Icon icon={mdiAllInclusive} size="24" />
<span class="uppercase">{allText}</span> <span>{allText}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
{#if refreshText} {#if refreshText}
<QueueCardButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}> <QueueCardButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}>
<Icon icon={mdiImageRefreshOutline} size="24" /> <Icon icon={mdiImageRefreshOutline} size="24" />
<span class="uppercase">{refreshText}</span> <span>{refreshText}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}> <QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
<Icon icon={mdiSelectionSearch} size="24" /> <Icon icon={mdiSelectionSearch} size="24" />
<span class="uppercase">{missingText}</span> <span>{missingText}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
{#if !disabled && !multipleButtons && isIdle} {#if !disabled && !multipleButtons && isIdle}
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}> <QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
<Icon icon={mdiPlay} size="48" /> <Icon icon={mdiPlay} size="48" />
<span class="uppercase">{missingText}</span> <span>{missingText}</span>
</QueueCardButton> </QueueCardButton>
{/if} {/if}
</div> </div>

View file

@ -12,7 +12,7 @@
import { handleSystemConfigSave } from '$lib/services/system-config.service'; import { handleSystemConfigSave } from '$lib/services/system-config.service';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk'; import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui'; import { Heading, LoadingSpinner, Text } from '@immich/ui';
import handlebar from 'handlebars'; import handlebar from 'handlebars';
import * as luxon from 'luxon'; import * as luxon from 'luxon';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
@ -158,7 +158,9 @@
{#if configToEdit.storageTemplate.enabled} {#if configToEdit.storageTemplate.enabled}
<hr /> <hr />
<h3 class="text-base font-medium text-primary">{$t('variables')}</h3> <Heading size="tiny" color="primary">
{$t('variables')}
</Heading>
<section class="support-date"> <section class="support-date">
{#await getSupportDateTimeFormat()} {#await getSupportDateTimeFormat()}
@ -174,11 +176,14 @@
<SupportedVariablesPanel /> <SupportedVariablesPanel />
</section> </section>
<div class="flex flex-col mt-4"> <div class="flex flex-col mt-2">
<h3 class="text-base font-medium text-primary">{$t('template')}</h3> <!-- <h3 class="text-base font-medium text-primary">{$t('template')}</h3> -->
<Heading size="tiny" color="primary">
{$t('template')}
</Heading>
<div class="my-2 text-sm"> <div class="my-2">
<h4 class="uppercase">{$t('preview')}</h4> <Text size="small">{$t('preview')}</Text>
</div> </div>
<p class="text-sm"> <p class="text-sm">
@ -249,7 +254,9 @@
{#if !minified} {#if !minified}
<div id="migration-info" class="mt-2 text-sm"> <div id="migration-info" class="mt-2 text-sm">
<h3 class="text-base font-medium text-primary">{$t('notes')}</h3> <Heading size="tiny" color="primary">
{$t('notes')}
</Heading>
<section class="flex flex-col gap-2"> <section class="flex flex-col gap-2">
<p> <p>
<FormatMessage <FormatMessage

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk'; import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
import { Card, CardBody, CardHeader, Text } from '@immich/ui';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -15,19 +16,20 @@
}; };
</script> </script>
<div class="mt-2 text-sm"> <Text size="small">{$t('date_and_time')}</Text>
<h4 class="uppercase">{$t('date_and_time')}</h4>
</div>
<!-- eslint-disable svelte/no-useless-mustaches --> <!-- eslint-disable svelte/no-useless-mustaches -->
<div class="mt-2 rounded-lg bg-gray-200 p-4 text-xs dark:bg-gray-700 dark:text-immich-dark-fg"> <Card class="mt-2 text-sm bg-light-50 shadow-none">
<div class="mb-2 text-gray-600 dark:text-immich-dark-fg"> <CardHeader>
<p>{$t('admin.storage_template_date_time_description')}</p> <Text class="mb-1">{$t('admin.storage_template_date_time_description')}</Text>
<p>{$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T20:03:05.250' } })}</p> <Text color="primary"
</div> >{$t('admin.storage_template_date_time_sample', { values: { date: '2022-02-03T20:03:05.250' } })}</Text
<div class="flex gap-10"> >
</CardHeader>
<CardBody>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-3 md:grid-cols-4">
<div> <div>
<p class="uppercase font-medium text-primary">{$t('year')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('year')}</Text>
<ul> <ul>
{#each options.yearOptions as yearFormat, index (index)} {#each options.yearOptions as yearFormat, index (index)}
<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li> <li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
@ -36,7 +38,7 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('month')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('month')}</Text>
<ul> <ul>
{#each options.monthOptions as monthFormat, index (index)} {#each options.monthOptions as monthFormat, index (index)}
<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li> <li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
@ -45,7 +47,7 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('week')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('week')}</Text>
<ul> <ul>
{#each options.weekOptions as weekFormat, index (index)} {#each options.weekOptions as weekFormat, index (index)}
<li>{'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}</li> <li>{'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}</li>
@ -54,7 +56,7 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('day')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('day')}</Text>
<ul> <ul>
{#each options.dayOptions as dayFormat, index (index)} {#each options.dayOptions as dayFormat, index (index)}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
@ -63,7 +65,7 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('hour')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('hour')}</Text>
<ul> <ul>
{#each options.hourOptions as dayFormat, index (index)} {#each options.hourOptions as dayFormat, index (index)}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
@ -72,7 +74,7 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('minute')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('minute')}</Text>
<ul> <ul>
{#each options.minuteOptions as dayFormat, index (index)} {#each options.minuteOptions as dayFormat, index (index)}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
@ -81,7 +83,7 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('second')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('second')}</Text>
<ul> <ul>
{#each options.secondOptions as dayFormat, index (index)} {#each options.secondOptions as dayFormat, index (index)}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li> <li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
@ -89,4 +91,5 @@
</ul> </ul>
</div> </div>
</div> </div>
</div> </CardBody>
</Card>

View file

@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { Card, CardBody, Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
</script> </script>
<div class="mt-4 text-sm"> <Text size="small">{$t('other_variables')}</Text>
<h4 class="uppercase">{$t('other_variables')}</h4>
</div>
<div class="p-4 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg"> <Card class="mt-2 text-sm bg-light-50 shadow-none">
<div class="flex gap-12"> <CardBody>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div> <div>
<p class="uppercase font-medium text-primary">{$t('filename')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('filename')}</Text>
<ul> <ul>
<li>{`{{filename}}`} - IMG_123</li> <li>{`{{filename}}`} - IMG_123</li>
<li>{`{{ext}}`} - jpg</li> <li>{`{{ext}}`} - jpg</li>
@ -17,14 +17,24 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('filetype')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('filetype')}</Text>
<ul> <ul>
<li>{`{{filetype}}`} - VID or IMG</li> <li>{`{{filetype}}`} - VID or IMG</li>
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li> <li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
</ul> </ul>
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('album')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('camera')}</Text>
<ul>
<li>{`{{make}}`} - FUJIFILM</li>
<li>{`{{model}}`} - X-T50</li>
<li>{`{{lensModel}}`} - XF27mm F2.8 R WR</li>
</ul>
</div>
<div>
<Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('album')}</Text>
<ul> <ul>
<li>{`{{album}}`} - Album Name</li> <li>{`{{album}}`} - Album Name</li>
<li> <li>
@ -38,19 +48,12 @@
</ul> </ul>
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('camera')}</p> <Text fontWeight="medium" size="tiny" color="primary" class="mb-1">{$t('other')}</Text>
<ul>
<li>{`{{make}}`} - FUJIFILM</li>
<li>{`{{model}}`} - X-T50</li>
<li>{`{{lensModel}}`} - XF27mm F2.8 R WR</li>
</ul>
</div>
<div>
<p class="uppercase font-medium text-primary">{$t('other')}</p>
<ul> <ul>
<li>{`{{assetId}}`} - Asset ID</li> <li>{`{{assetId}}`} - Asset ID</li>
<li>{`{{assetIdShort}}`} - Asset ID (last 12 characters)</li> <li>{`{{assetIdShort}}`} - Asset ID (last 12 characters)</li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </CardBody>
</Card>

View file

@ -5,7 +5,7 @@
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { removeTag } from '$lib/utils/asset-utils'; import { removeTag } from '$lib/utils/asset-utils';
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { Icon, modalManager } from '@immich/ui'; import { Icon, modalManager, Text } from '@immich/ui';
import { mdiClose, mdiPlus } from '@mdi/js'; import { mdiClose, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -38,7 +38,7 @@
{#if isOwner && !authManager.isSharedLink} {#if isOwner && !authManager.isSharedLink}
<section class="px-4 mt-4"> <section class="px-4 mt-4">
<div class="flex h-10 w-full items-center justify-between text-sm"> <div class="flex h-10 w-full items-center justify-between text-sm">
<h2 class="uppercase">{$t('tags')}</h2> <Text color="muted">{$t('tags')}</Text>
</div> </div>
<section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags"> <section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags">
{#each tags as tag (tag.id)} {#each tags as tag (tag.id)}

View file

@ -20,7 +20,7 @@
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util'; import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
import { getParentPath } from '$lib/utils/tree-utils'; import { getParentPath } from '$lib/utils/tree-utils';
import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk'; import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui'; import { Icon, IconButton, LoadingSpinner, modalManager, Text } from '@immich/ui';
import { import {
mdiCalendar, mdiCalendar,
mdiCamera, mdiCamera,
@ -160,7 +160,7 @@
{#if !authManager.isSharedLink && isOwner} {#if !authManager.isSharedLink && isOwner}
<section class="px-4 pt-4 text-sm"> <section class="px-4 pt-4 text-sm">
<div class="flex h-10 w-full items-center justify-between"> <div class="flex h-10 w-full items-center justify-between">
<h2 class="uppercase">{$t('people')}</h2> <Text size="small" color="muted">{$t('people')}</Text>
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
{#if people.some((person) => person.isHidden)} {#if people.some((person) => person.isHidden)}
<IconButton <IconButton
@ -259,10 +259,10 @@
<div class="px-4 py-4"> <div class="px-4 py-4">
{#if asset.exifInfo} {#if asset.exifInfo}
<div class="flex h-10 w-full items-center justify-between text-sm"> <div class="flex h-10 w-full items-center justify-between text-sm">
<h2 class="uppercase">{$t('details')}</h2> <Text size="small" color="muted">{$t('details')}</Text>
</div> </div>
{:else} {:else}
<p class="uppercase text-sm">{$t('no_exif_info_available')}</p> <Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
{/if} {/if}
{#if dateTime} {#if dateTime}
@ -487,7 +487,7 @@
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner} {#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
<section class="px-6 dark:text-immich-dark-fg mt-4"> <section class="px-6 dark:text-immich-dark-fg mt-4">
<p class="uppercase text-sm">{$t('shared_by')}</p> <Text size="small" color="muted">{$t('shared_by')}</Text>
<div class="flex gap-4 pt-4"> <div class="flex gap-4 pt-4">
<div> <div>
<UserAvatar user={asset.owner} size="md" /> <UserAvatar user={asset.owner} size="md" />
@ -504,7 +504,9 @@
{#if albums.length > 0} {#if albums.length > 0}
<section class="px-6 py-6 dark:text-immich-dark-fg"> <section class="px-6 py-6 dark:text-immich-dark-fg">
<p class="uppercase pb-4 text-sm">{$t('appears_in')}</p> <div class="pb-4">
<Text size="small" color="muted">{$t('appears_in')}</Text>
</div>
{#each albums as album (album.id)} {#each albums as album (album.id)}
<a href={Route.viewAlbum(album)}> <a href={Route.viewAlbum(album)}>
<div class="flex gap-4 pt-2 hover:cursor-pointer items-center"> <div class="flex gap-4 pt-2 hover:cursor-pointer items-center">

View file

@ -147,7 +147,7 @@
{/if} {/if}
</div> </div>
<div class="px-4 py-4 text-sm"> <div class="px-4 py-4 text-sm">
<h2 class="mb-8 mt-4 uppercase">{$t('all_people')}</h2> <h2 class="mb-8 mt-4">{$t('all_people')}</h2>
{#if isShowLoadingPeople} {#if isShowLoadingPeople}
<div class="flex w-full justify-center"> <div class="flex w-full justify-center">
<LoadingSpinner /> <LoadingSpinner />

View file

@ -39,7 +39,7 @@
<Icon {icon} size="30" class="text-primary" /> <Icon {icon} size="30" class="text-primary" />
{/if} {/if}
{#if title} {#if title}
<p class="uppercase text-xl text-primary"> <p class="text-xl text-primary font-medium">
{title} {title}
</p> </p>
{/if} {/if}

View file

@ -19,7 +19,7 @@
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary" class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
> >
<Icon icon={sunPath} viewBox={sunViewBox} size="96" /> <Icon icon={sunPath} viewBox={sunViewBox} size="96" />
<p class="uppercase font-semibold text-4xl">{$t('light')}</p> <p class="font-semibold text-4xl">{$t('light')}</p>
</div> </div>
</button> </button>
<button <button
@ -31,7 +31,7 @@
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary" class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
> >
<Icon icon={moonPath} viewBox={moonViewBox} size="96" /> <Icon icon={moonPath} viewBox={moonViewBox} size="96" />
<p class="uppercase font-semibold text-4xl">{$t('dark')}</p> <p class="font-semibold text-4xl">{$t('dark')}</p>
</div> </div>
</button> </button>
</div> </div>

View file

@ -24,7 +24,7 @@
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { Icon, IconButton, Label } from '@immich/ui'; import { Icon, IconButton, Label } from '@immich/ui';
import { mdiClose, mdiMagnify, mdiUnfoldMoreHorizontal } from '@mdi/js'; import { mdiChevronDown, mdiClose, mdiMagnify } from '@mdi/js';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { FormEventHandler } from 'svelte/elements'; import type { FormEventHandler } from 'svelte/elements';
@ -251,7 +251,7 @@
</script> </script>
<svelte:window onresize={onPositionChange} /> <svelte:window onresize={onPositionChange} />
<Label class="block mb-1 {hideLabel ? 'sr-only' : ''}" for={inputId}>{label}</Label> <Label class="block mb-1 {hideLabel ? 'sr-only' : ''} text-xs text-neutral-500 font-light" for={inputId}>{label}</Label>
<div <div
class="relative w-full dark:text-gray-300 text-gray-700 text-base" class="relative w-full dark:text-gray-300 text-gray-700 text-base"
use:focusOutside={{ onFocusOut: deactivate }} use:focusOutside={{ onFocusOut: deactivate }}
@ -351,7 +351,7 @@
size="small" size="small"
/> />
{:else if !isOpen} {:else if !isOpen}
<Icon icon={mdiUnfoldMoreHorizontal} aria-hidden /> <Icon icon={mdiChevronDown} aria-hidden />
{/if} {/if}
</div> </div>
</div> </div>
@ -391,7 +391,7 @@
<li <li
aria-selected={index === selectedIndex} aria-selected={index === selectedIndex}
bind:this={optionRefs[index]} bind:this={optionRefs[index]}
class="text-start w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700 break-words" class="text-start w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700 wrap-break-words"
id={`${listboxId}-${index}`} id={`${listboxId}-${index}`}
onclick={() => handleSelect(option)} onclick={() => handleSelect(option)}
role="option" role="option"

View file

@ -10,6 +10,7 @@
import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte'; import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk'; import { SearchSuggestionType, getSearchSuggestions } from '@immich/sdk';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -81,8 +82,7 @@
</script> </script>
<div id="camera-selection"> <div id="camera-selection">
<p class="uppercase immich-form-label">{$t('camera')}</p> <Text fontWeight="medium">{$t('camera')}</Text>
<div class="grid grid-auto-fit-40 gap-5 mt-1"> <div class="grid grid-auto-fit-40 gap-5 mt-1">
<div class="w-full"> <div class="w-full">
<Combobox <Combobox

View file

@ -1,13 +1,13 @@
<script lang="ts" module> <script lang="ts" module>
export interface SearchDateFilter { export interface SearchDateFilter {
takenBefore?: string; takenBefore?: DateTime;
takenAfter?: string; takenAfter?: DateTime;
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import DateInput from '$lib/elements/DateInput.svelte'; import { DatePicker, Text } from '@immich/ui';
import { Text } from '@immich/ui'; import type { DateTime } from 'luxon';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -17,23 +17,19 @@
let { filters = $bindable() }: Props = $props(); let { filters = $bindable() }: Props = $props();
let invalid = $derived(filters.takenAfter && filters.takenBefore && filters.takenAfter > filters.takenBefore); let invalid = $derived(filters.takenAfter && filters.takenBefore && filters.takenAfter > filters.takenBefore);
const inputClasses = $derived(
`immich-form-input w-full mt-1 hover:cursor-pointer ${invalid ? 'border border-danger' : ''}`,
);
</script> </script>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div id="date-range-selection" class="grid grid-auto-fit-40 gap-5"> <div id="date-range-selection" class="grid grid-auto-fit-40 gap-5">
<label class="immich-form-label" for="start-date"> <div>
<span class="uppercase">{$t('start_date')}</span> <Text class="mb-2" fontWeight="medium">{$t('start_date')}</Text>
<DateInput class={inputClasses} type="date" id="start-date" name="start-date" bind:value={filters.takenAfter} /> <DatePicker bind:value={filters.takenAfter} />
</label> </div>
<label class="immich-form-label" for="end-date"> <div>
<span class="uppercase">{$t('end_date')}</span> <Text class="mb-2" fontWeight="medium">{$t('end_date')}</Text>
<DateInput class={inputClasses} type="date" id="end-date" name="end-date" bind:value={filters.takenBefore} /> <DatePicker bind:value={filters.takenBefore} />
</label> </div>
</div> </div>
{#if invalid} {#if invalid}
<Text color="danger">{$t('start_date_before_end_date')}</Text> <Text color="danger">{$t('start_date_before_end_date')}</Text>

View file

@ -7,7 +7,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { Checkbox, Label } from '@immich/ui'; import { Checkbox, Label, Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -20,19 +20,20 @@
<div id="display-options-selection"> <div id="display-options-selection">
<fieldset> <fieldset>
<legend class="uppercase immich-form-label">{$t('display_options')}</legend> <Text class="mb-2" fontWeight="medium">{$t('display_options')}</Text>
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1"> <div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox id="not-in-album-checkbox" size="tiny" bind:checked={filters.isNotInAlbum} /> <Checkbox id="not-in-album-checkbox" size="tiny" bind:checked={filters.isNotInAlbum} />
<Label label={$t('not_in_any_album')} for="not-in-album-checkbox" /> <Label label={$t('not_in_any_album')} for="not-in-album-checkbox" class="text-sm font-normal" />
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox id="archive-checkbox" size="tiny" bind:checked={filters.isArchive} /> <Checkbox id="archive-checkbox" size="tiny" bind:checked={filters.isArchive} />
<Label label={$t('archive')} for="archive-checkbox" /> <Label label={$t('archive')} for="archive-checkbox" class="text-sm font-normal" />
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox id="favorites-checkbox" size="tiny" bind:checked={filters.isFavorite} /> <Checkbox id="favorites-checkbox" size="tiny" bind:checked={filters.isFavorite} />
<Label label={$t('favorites')} for="favorites-checkbox" /> <Label label={$t('favorites')} for="favorites-checkbox" class="text-sm font-normal" />
</div> </div>
</div> </div>
</fieldset> </fieldset>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { searchStore } from '$lib/stores/search.svelte'; import { searchStore } from '$lib/stores/search.svelte';
import { Icon, IconButton } from '@immich/ui'; import { Icon, IconButton, Text } from '@immich/ui';
import { mdiClose, mdiMagnify } from '@mdi/js'; import { mdiClose, mdiMagnify } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@ -97,7 +97,7 @@
class="absolute w-full rounded-b-3xl border-2 border-t-0 border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-700 dark:bg-immich-dark-gray dark:text-gray-300 z-1" class="absolute w-full rounded-b-3xl border-2 border-t-0 border-gray-200 bg-white pb-5 shadow-2xl transition-all dark:border-gray-700 dark:bg-immich-dark-gray dark:text-gray-300 z-1"
> >
<div class="flex items-center justify-between px-5 pt-5 text-xs"> <div class="flex items-center justify-between px-5 pt-5 text-xs">
<p class="uppercase py-2" aria-hidden={true}>{$t('recent_searches')}</p> <Text class="py-2" color="muted" aria-hidden={true}>{$t('recent_searches')}</Text>
{#if showClearAll} {#if showClearAll}
<button <button
id={getId(0)} id={getId(0)}

View file

@ -10,6 +10,7 @@
import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte'; import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk'; import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
import { Text } from '@immich/ui';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -74,7 +75,7 @@
</script> </script>
<div id="location-selection"> <div id="location-selection">
<p class="uppercase immich-form-label">{$t('place')}</p> <Text fontWeight="medium">{$t('place')}</Text>
<div class="grid grid-auto-fit-40 gap-5 mt-1"> <div class="grid grid-auto-fit-40 gap-5 mt-1">
<div class="w-full"> <div class="w-full">

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { MediaType } from '$lib/constants'; import { MediaType } from '$lib/constants';
import RadioButton from '$lib/elements/RadioButton.svelte'; import RadioButton from '$lib/elements/RadioButton.svelte';
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -12,7 +13,8 @@
<div id="media-type-selection"> <div id="media-type-selection">
<fieldset> <fieldset>
<legend class="uppercase immich-form-label">{$t('media_type')}</legend> <Text class="mb-2" fontWeight="medium">{$t('media_type')}</Text>
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1"> <div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
<RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label={$t('all')} value={MediaType.All} /> <RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label={$t('all')} value={MediaType.All} />
<RadioButton <RadioButton

View file

@ -5,7 +5,7 @@
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getAllPeople, type PersonResponseDto } from '@immich/sdk'; import { getAllPeople, type PersonResponseDto } from '@immich/sdk';
import { Button, LoadingSpinner } from '@immich/ui'; import { Button, LoadingSpinner, Text } from '@immich/ui';
import { mdiArrowRight, mdiClose } from '@mdi/js'; import { mdiArrowRight, mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { SvelteSet } from 'svelte/reactivity'; import type { SvelteSet } from 'svelte/reactivity';
@ -63,12 +63,12 @@
<div id="people-selection" class="max-h-60 -mb-4 overflow-y-auto immich-scrollbar"> <div id="people-selection" class="max-h-60 -mb-4 overflow-y-auto immich-scrollbar">
<div class="flex items-center w-full justify-between gap-6"> <div class="flex items-center w-full justify-between gap-6">
<p class="uppercase immich-form-label py-3">{$t('people')}</p> <Text class="py-3" fontWeight="medium">{$t('people')}</Text>
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} /> <SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
</div> </div>
<SingleGridRow <SingleGridRow
class="grid grid-auto-fill-20 gap-1 mt-2 overflow-y-auto immich-scrollbar" class="grid grid-auto-fill-20 gap-1 mt-2 overflow-y-auto immich-scrollbar space-between"
bind:itemCount={numberOfPeople} bind:itemCount={numberOfPeople}
> >
{#each peopleList as person (person.id)} {#each peopleList as person (person.id)}

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import Combobox from '../combobox.svelte'; import Combobox from '../combobox.svelte';
@ -18,16 +19,14 @@
]; ];
</script> </script>
<div class="grid grid-auto-fit-40 gap-5"> <div class="flex flex-col">
<label class="immich-form-label" for="start-date"> <Text class="mb-2" fontWeight="medium">{$t('rating')}</Text>
<div class="[&_label]:uppercase">
<Combobox <Combobox
label={$t('rating')} label={$t('rating')}
placeholder={$t('search_rating')} placeholder={$t('search_rating')}
hideLabel
{options} {options}
selectedOption={rating === undefined ? undefined : options[rating]} selectedOption={rating === undefined ? undefined : options[rating]}
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))} onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
/> />
</div>
</label>
</div> </div>

View file

@ -2,7 +2,7 @@
import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { getAllTags, type TagResponseDto } from '@immich/sdk'; import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { Checkbox, Icon, Label } from '@immich/ui'; import { Checkbox, Icon, Label, Text } from '@immich/ui';
import { mdiClose } from '@mdi/js'; import { mdiClose } from '@mdi/js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -43,10 +43,11 @@
{#if $preferences?.tags?.enabled} {#if $preferences?.tags?.enabled}
<div id="location-selection"> <div id="location-selection">
<form autocomplete="off" id="create-tag-form"> <form autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2"> <div class="mb-4 flex flex-col">
<div class="[&_label]:uppercase"> <Text class="py-3" fontWeight="medium">{$t('tags')}</Text>
<Combobox <Combobox
disabled={selectedTags === null} disabled={selectedTags === null}
hideLabel
onSelect={handleSelect} onSelect={handleSelect}
label={$t('tags')} label={$t('tags')}
defaultFirstOption defaultFirstOption
@ -55,7 +56,6 @@
placeholder={$t('search_tags')} placeholder={$t('search_tags')}
/> />
</div> </div>
</div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox <Checkbox
id="untagged-checkbox" id="untagged-checkbox"
@ -65,7 +65,7 @@
selectedTags = checked ? null : new SvelteSet(); selectedTags = checked ? null : new SvelteSet();
}} }}
/> />
<Label label={$t('untagged')} for="untagged-checkbox" /> <Label label={$t('untagged')} for="untagged-checkbox" class="text-sm font-normal" />
</div> </div>
</form> </form>

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import RadioButton from '$lib/elements/RadioButton.svelte'; import RadioButton from '$lib/elements/RadioButton.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Field, Input, Text } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -11,9 +12,10 @@
let { query = $bindable(), queryType = $bindable('smart') }: Props = $props(); let { query = $bindable(), queryType = $bindable('smart') }: Props = $props();
</script> </script>
<fieldset> <section>
<legend class="immich-form-label">{$t('search_type')}</legend> <fieldset>
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1 mb-2"> <Text class="mb-2" fontWeight="medium">{$t('search_type')}</Text>
<div class="flex flex-wrap gap-x-5 gap-y-2 my-2">
{#if featureFlagsManager.value.smartSearch} {#if featureFlagsManager.value.smartSearch}
<RadioButton name="query-type" id="context-radio" label={$t('context')} bind:group={queryType} value="smart" /> <RadioButton name="query-type" id="context-radio" label={$t('context')} bind:group={queryType} value="smart" />
{/if} {/if}
@ -35,49 +37,23 @@
<RadioButton name="query-type" id="ocr-radio" label={$t('ocr')} bind:group={queryType} value="ocr" /> <RadioButton name="query-type" id="ocr-radio" label={$t('ocr')} bind:group={queryType} value="ocr" />
{/if} {/if}
</div> </div>
</fieldset> </fieldset>
{#if queryType === 'smart'} {#if queryType === 'smart'}
<label for="context-input" class="immich-form-label">{$t('search_by_context')}</label> <Field label={$t('search_by_context')}>
<input <Input type="text" placeholder={$t('sunrise_on_the_beach')} bind:value={query} />
class="immich-form-input hover:cursor-text w-full mt-1!" </Field>
type="text" {:else if queryType === 'metadata'}
id="context-input" <Field label={$t('search_by_filename')}>
name="context" <Input type="text" placeholder={$t('search_by_filename_example')} bind:value={query} />
placeholder={$t('sunrise_on_the_beach')} </Field>
bind:value={query} {:else if queryType === 'description'}
/> <Field label={$t('search_by_description')}>
{:else if queryType === 'metadata'} <Input type="text" placeholder={$t('search_by_description_example')} bind:value={query} />
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label> </Field>
<input {:else if queryType === 'ocr'}
class="immich-form-input hover:cursor-text w-full mt-1!" <Field label={$t('search_by_ocr')}>
type="text" <Input type="text" placeholder={$t('search_by_ocr_example')} bind:value={query} />
id="file-name-input" </Field>
name="file-name" {/if}
placeholder={$t('search_by_filename_example')} </section>
bind:value={query}
aria-labelledby="file-name-label"
/>
{:else if queryType === 'description'}
<label for="description-input" class="immich-form-label">{$t('search_by_description')}</label>
<input
class="immich-form-input hover:cursor-text w-full mt-1!"
type="text"
id="description-input"
name="description"
placeholder={$t('search_by_description_example')}
bind:value={query}
aria-labelledby="description-label"
/>
{:else if queryType === 'ocr'}
<label for="ocr-input" class="immich-form-label">{$t('search_by_ocr')}</label>
<input
class="immich-form-input hover:cursor-text w-full mt-1!"
type="text"
id="ocr-input"
name="ocr"
placeholder={$t('search_by_ocr_example')}
bind:value={query}
aria-labelledby="ocr-label"
/>
{/if}

View file

@ -81,7 +81,7 @@
<div class="mb-4 w-full"> <div class="mb-4 w-full">
<div class="flex place-items-center gap-1"> <div class="flex place-items-center gap-1">
<label class="font-medium text-primary text-sm min-h-6 uppercase" for={label}>{label}</label> <label class="font-medium text-primary text-sm min-h-6" for={label}>{label}</label>
{#if required} {#if required}
<div class="text-red-400">*</div> <div class="text-red-400">*</div>
{/if} {/if}

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { deleteAllSessions, deleteSession, getSessions, type SessionResponseDto } from '@immich/sdk'; import { deleteAllSessions, deleteSession, getSessions, type SessionResponseDto } from '@immich/sdk';
import { Button, modalManager, toastManager } from '@immich/ui'; import { Button, modalManager, Text, toastManager } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import DeviceCard from './device-card.svelte'; import DeviceCard from './device-card.svelte';
@ -52,17 +52,17 @@
<section class="my-4"> <section class="my-4">
{#if currentSession} {#if currentSession}
<div class="mb-6"> <div class="mb-6">
<h3 class="uppercase mb-2 text-xs font-medium text-primary"> <Text class="mb-2" fontWeight="medium" size="tiny" color="primary">
{$t('current_device')} {$t('current_device')}
</h3> </Text>
<DeviceCard session={currentSession} /> <DeviceCard session={currentSession} />
</div> </div>
{/if} {/if}
{#if otherSessions.length > 0} {#if otherSessions.length > 0}
<div class="mb-6"> <div class="mb-6">
<h3 class="uppercase mb-2 text-xs font-medium text-primary"> <Text class="mb-2" fontWeight="medium" size="tiny" color="primary">
{$t('other_devices')} {$t('other_devices')}
</h3> </Text>
{#each otherSessions as session, index (session.id)} {#each otherSessions as session, index (session.id)}
<DeviceCard {session} onDelete={() => handleDelete(session)} /> <DeviceCard {session} onDelete={() => handleDelete(session)} />
{#if index !== otherSessions.length - 1} {#if index !== otherSessions.length - 1}
@ -70,9 +70,11 @@
{/if} {/if}
{/each} {/each}
</div> </div>
<h3 class="uppercase mb-2 text-xs font-medium text-primary">
{$t('log_out_all_devices')} <div class="my-3">
</h3> <hr />
</div>
<div class="flex justify-end"> <div class="flex justify-end">
<Button shape="round" color="danger" size="small" onclick={handleDeleteAll}>{$t('log_out_all_devices')}</Button> <Button shape="round" color="danger" size="small" onclick={handleDeleteAll}>{$t('log_out_all_devices')}</Button>
</div> </div>

View file

@ -12,7 +12,7 @@
type PartnerResponseDto, type PartnerResponseDto,
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Button, Icon, IconButton, modalManager } from '@immich/ui'; import { Button, Icon, IconButton, modalManager, Text } from '@immich/ui';
import { mdiCheck, mdiClose } from '@mdi/js'; import { mdiCheck, mdiClose } from '@mdi/js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -157,10 +157,12 @@
<!-- I am sharing my assets with this user --> <!-- I am sharing my assets with this user -->
{#if partner.sharedByMe} {#if partner.sharedByMe}
<hr class="my-4 border border-gray-200 dark:border-gray-700" /> <hr class="my-4 border border-gray-200 dark:border-gray-700" />
<p class="uppercase text-xs font-medium my-4"> <Text class="my-4" size="small" fontWeight="medium">
{$t('shared_with_partner', { values: { partner: partner.user.name } })} {$t('shared_with_partner', { values: { partner: partner.user.name } })}
</p> </Text>
<p class="text-md">{$t('partner_can_access', { values: { partner: partner.user.name } })}</p> <Text size="tiny" fontWeight="medium"
>{$t('partner_can_access', { values: { partner: partner.user.name } })}</Text
>
<ul class="text-sm"> <ul class="text-sm">
<li class="flex gap-2 place-items-center py-1 mt-2"> <li class="flex gap-2 place-items-center py-1 mt-2">
<Icon icon={mdiCheck} /> <Icon icon={mdiCheck} />
@ -176,9 +178,10 @@
<!-- this user is sharing assets with me --> <!-- this user is sharing assets with me -->
{#if partner.sharedWithMe} {#if partner.sharedWithMe}
<hr class="my-4 border border-gray-200 dark:border-gray-700" /> <hr class="my-4 border border-gray-200 dark:border-gray-700" />
<p class="uppercase text-xs font-medium my-4"> <Text class="my-4" size="small" fontWeight="medium">
{$t('shared_from_partner', { values: { partner: partner.user.name } })} {$t('shared_from_partner', { values: { partner: partner.user.name } })}
</p> </Text>
<SettingSwitch <SettingSwitch
title={$t('show_in_timeline')} title={$t('show_in_timeline')}
subtitle={$t('show_in_timeline_setting_description')} subtitle={$t('show_in_timeline_setting_description')}

View file

@ -2,7 +2,7 @@
import AppDownloadModal from '$lib/modals/AppDownloadModal.svelte'; import AppDownloadModal from '$lib/modals/AppDownloadModal.svelte';
import ObtainiumConfigModal from '$lib/modals/ObtainiumConfigModal.svelte'; import ObtainiumConfigModal from '$lib/modals/ObtainiumConfigModal.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { Icon, modalManager } from '@immich/ui'; import { Icon, modalManager, Text } from '@immich/ui';
import { import {
mdiCellphoneArrowDownVariant, mdiCellphoneArrowDownVariant,
mdiContentDuplicate, mdiContentDuplicate,
@ -22,7 +22,7 @@
</script> </script>
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white"> <div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<p class="uppercase text-xs font-medium p-4">{$t('organize_your_library')}</p> <Text size="tiny" color="muted" fontWeight="medium" class="p-4">{$t('organize_your_library')}</Text>
{#each links as link (link.href)} {#each links as link (link.href)}
<a href={link.href} class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"> <a href={link.href} class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4">
@ -33,7 +33,8 @@
</div> </div>
<br /> <br />
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white"> <div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<p class="uppercase text-xs font-medium p-4">{$t('download')}</p> <Text size="tiny" color="muted" fontWeight="medium" class="p-4">{$t('download')}</Text>
<button <button
type="button" type="button"
onclick={() => modalManager.show(ObtainiumConfigModal, {})} onclick={() => modalManager.show(ObtainiumConfigModal, {})}

View file

@ -113,7 +113,7 @@
<div class="rounded-lg bg-light-100 border p-3"> <div class="rounded-lg bg-light-100 border p-3">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<Icon icon={mdiFlashOutline} size="18" class="text-primary" /> <Icon icon={mdiFlashOutline} size="18" class="text-primary" />
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('trigger')}</span> <Text size="tiny" fontWeight="semi-bold">{$t('trigger')}</Text>
</div> </div>
<p class="text-sm truncate pl-5">{getTriggerName(trigger.type)}</p> <p class="text-sm truncate pl-5">{getTriggerName(trigger.type)}</p>
</div> </div>
@ -128,7 +128,7 @@
<div class="rounded-lg bg-light-100 border p-3"> <div class="rounded-lg bg-light-100 border p-3">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<Icon icon={mdiFilterOutline} size="18" class="text-warning" /> <Icon icon={mdiFilterOutline} size="18" class="text-warning" />
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('filters')}</span> <Text size="tiny" fontWeight="semi-bold">{$t('filters')}</Text>
</div> </div>
<div class="space-y-1 pl-5"> <div class="space-y-1 pl-5">
{#each filters as filter, index (index)} {#each filters as filter, index (index)}
@ -154,7 +154,7 @@
<div class="rounded-lg bg-light-100 border p-3"> <div class="rounded-lg bg-light-100 border p-3">
<div class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<Icon icon={mdiPlayCircleOutline} size="18" class="text-success" /> <Icon icon={mdiPlayCircleOutline} size="18" class="text-success" />
<span class="text-[10px] font-semibold uppercase tracking-wide">{$t('actions')}</span> <Text size="tiny" fontWeight="semi-bold">{$t('actions')}</Text>
</div> </div>
<div class="space-y-1 pl-5"> <div class="space-y-1 pl-5">
{#each actions as action, index (index)} {#each actions as action, index (index)}

View file

@ -12,5 +12,5 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input type="radio" {name} {id} {value} class="focus-visible:ring" bind:group /> <input type="radio" {name} {id} {value} class="focus-visible:ring" bind:group />
<label for={id}>{label}</label> <label for={id} class="text-sm">{label}</label>
</div> </div>

View file

@ -37,6 +37,7 @@
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk'; import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui'; import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
import { mdiTune } from '@mdi/js'; import { mdiTune } from '@mdi/js';
import type { DateTime } from 'luxon';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
@ -47,8 +48,8 @@
let { searchQuery, onClose }: Props = $props(); let { searchQuery, onClose }: Props = $props();
const parseOptionalDate = (dateString?: string) => (dateString ? parseUtcDate(dateString) : undefined); const parseOptionalDate = (dateString?: DateTime) => (dateString ? parseUtcDate(dateString.toString()) : undefined);
const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined; const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day') || undefined;
const formId = generateId(); const formId = generateId();
// combobox and all the search components have terrible support for value | null so we use empty string instead. // combobox and all the search components have terrible support for value | null so we use empty string instead.
@ -187,7 +188,7 @@
<Modal icon={mdiTune} size="giant" title={$t('search_options')} {onClose}> <Modal icon={mdiTune} size="giant" title={$t('search_options')} {onClose}>
<ModalBody> <ModalBody>
<form id={formId} autocomplete="off" {onsubmit} {onreset}> <form id={formId} autocomplete="off" {onsubmit} {onreset}>
<div class="flex flex-col gap-4 pb-10" tabindex="-1"> <div class="flex flex-col gap-5 pb-10" tabindex="-1">
<!-- PEOPLE --> <!-- PEOPLE -->
<SearchPeopleSection bind:selectedPeople={filter.personIds} /> <SearchPeopleSection bind:selectedPeople={filter.personIds} />

View file

@ -446,3 +446,17 @@ export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
actions.map((action) => ({ ...action, icon: undefined })); actions.map((action) => ({ ...action, icon: undefined }));
export const isEnabled = ({ $if }: IfLike) => $if?.() ?? true; export const isEnabled = ({ $if }: IfLike) => $if?.() ?? true;
export const transformToTitleCase = (text: string) => {
if (text.length === 0) {
return text;
} else if (text.length === 1) {
return text.charAt(0).toUpperCase();
}
let result = '';
for (const word of text.toLowerCase().split(' ')) {
result += word.charAt(0).toUpperCase() + word.slice(1) + ' ';
}
return result.trim();
};

View file

@ -28,7 +28,7 @@
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { joinPaths } from '$lib/utils/tree-utils'; import { joinPaths } from '$lib/utils/tree-utils';
import { IconButton } from '@immich/ui'; import { IconButton, Text } from '@immich/ui';
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -77,7 +77,7 @@
<Sidebar> <Sidebar>
<SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" /> <SkipLink target={`#${headerId}`} text={$t('skip_to_folders')} breakpoint="md" />
<section> <section>
<div class="uppercase text-xs ps-4 mb-2 dark:text-white">{$t('explorer')}</div> <Text class="ps-4 mb-4" size="small">{$t('explorer')}</Text>
<div class="h-full"> <div class="h-full">
<TreeItems <TreeItems
icons={{ default: mdiFolderOutline, active: mdiFolder }} icons={{ default: mdiFolderOutline, active: mdiFolder }}

View file

@ -2,7 +2,6 @@
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { shortcut } from '$lib/actions/shortcut'; import { shortcut } from '$lib/actions/shortcut';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
@ -308,16 +307,6 @@
bind:clientWidth={viewport.width} bind:clientWidth={viewport.width}
bind:this={searchResultsElement} bind:this={searchResultsElement}
> >
{#if searchResultAlbums.length > 0}
<section>
<div class="uppercase ms-6 text-4xl font-medium text-black/70 dark:text-white/80">{$t('albums')}</div>
<AlbumCardGroup albums={searchResultAlbums} showDateRange showItemCount />
<div class="uppercase m-6 text-4xl font-medium text-black/70 dark:text-white/80">
{$t('photos_and_videos')}
</div>
</section>
{/if}
<section id="search-content"> <section id="search-content">
{#if searchResultAssets.length > 0} {#if searchResultAssets.length > 0}
<GalleryViewer <GalleryViewer

View file

@ -30,6 +30,7 @@
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { joinPaths, TreeNode } from '$lib/utils/tree-utils'; import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
import { getAllTags, type TagResponseDto } from '@immich/sdk'; import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { Text } from '@immich/ui';
import { mdiDotsVertical, mdiPlus, mdiTag, mdiTagMultiple } from '@mdi/js'; import { mdiDotsVertical, mdiPlus, mdiTag, mdiTagMultiple } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -82,7 +83,7 @@
<Sidebar> <Sidebar>
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" /> <SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
<section> <section>
<div class="uppercase text-xs ps-4 mb-2 dark:text-white">{$t('explorer')}</div> <Text class="ps-4 mb-4" size="small">{$t('explorer')}</Text>
<div class="h-full"> <div class="h-full">
<TreeItems icons={{ default: mdiTag, active: mdiTag }} {tree} active={tag.path} {getLink} /> <TreeItems icons={{ default: mdiTag, active: mdiTag }} {tree} active={tag.path} {getLink} />
</div> </div>

View file

@ -139,9 +139,9 @@
<UserPageLayout title={data.meta.title} scrollbar={true}> <UserPageLayout title={data.meta.title} scrollbar={true}>
{#snippet buttons()} {#snippet buttons()}
<div class="flex gap-2 justify-end place-items-center"> <div class="flex gap-2 justify-end place-items-center">
<Text class="hidden md:block text-xs mr-4 text-dark/50">{$t('geolocation_instruction_location')}</Text> <Text class="hidden md:block mr-4" size="tiny" color="muted">{$t('geolocation_instruction_location')}</Text>
<div class="border flex place-items-center place-content-center px-2 py-1 bg-primary/10 rounded-2xl"> <div class="border flex place-items-center place-content-center px-2 py-1 bg-primary/10 rounded-2xl">
<Text class="hidden md:inline-block text-xs text-gray-500 font-mono mr-5 ml-2 uppercase"> <Text class="hidden md:inline-block font-mono mr-5 ml-2" color="muted" size="tiny">
{$t('selected_gps_coordinates')} {$t('selected_gps_coordinates')}
</Text> </Text>
<Text <Text

View file

@ -208,9 +208,7 @@
<!-- Trigger Section --> <!-- Trigger Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200"> <div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3"> <div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold" <Text size="tiny" color="muted" fontWeight="medium">{$t('trigger')}</Text>
>{$t('trigger')}</Text
>
</div> </div>
{@render chipItem(getTriggerLabel(workflow.triggerType))} {@render chipItem(getTriggerLabel(workflow.triggerType))}
</div> </div>
@ -218,9 +216,7 @@
<!-- Filters Section --> <!-- Filters Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200"> <div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3"> <div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold" <Text size="tiny" color="muted" fontWeight="medium">{$t('filters')}</Text>
>{$t('filters')}</Text
>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#if workflow.filters.length === 0} {#if workflow.filters.length === 0}
@ -238,9 +234,7 @@
<!-- Actions Section --> <!-- Actions Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200"> <div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3"> <div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold" <Text size="tiny" color="muted" fontWeight="medium">{$t('actions')}</Text>
>{$t('actions')}</Text
>
</div> </div>
<div> <div>

View file

@ -338,7 +338,7 @@
<div class="w-full border-t-2 border-dashed border-light-200"></div> <div class="w-full border-t-2 border-dashed border-light-200"></div>
</div> </div>
<div class="relative flex justify-center text-xs uppercase"> <div class="relative flex justify-center text-xs uppercase">
<span class="bg-white dark:bg-black px-2 font-semibold text-light-500">THEN</span> <Text class="bg-white dark:bg-black px-2" fontWeight="semi-bold" size="tiny" color="muted">{$t('then')}</Text>
</div> </div>
</div> </div>
{/snippet} {/snippet}