Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Tran
0a62435d5a chore: lower case text 2025-12-28 04:25:17 +00:00
13 changed files with 88 additions and 83 deletions

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

@@ -175,7 +175,7 @@
<h3 class="text-base font-medium text-primary">{$t('template')}</h3> <h3 class="text-base font-medium text-primary">{$t('template')}</h3>
<div class="my-2 text-sm"> <div class="my-2 text-sm">
<h4 class="uppercase">{$t('preview')}</h4> <h4 class="text-sm">{$t('preview')}</h4>
</div> </div>
<p class="text-sm"> <p class="text-sm">

View File

@@ -3,13 +3,13 @@
</script> </script>
<div class="mt-4 text-sm"> <div class="mt-4 text-sm">
<h4 class="uppercase">{$t('other_variables')}</h4> <h4 class="">{$t('other_variables')}</h4>
</div> </div>
<div class="p-4 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg"> <div class="p-4 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
<div class="flex gap-12"> <div class="flex gap-12">
<div> <div>
<p class="uppercase font-medium text-primary">{$t('filename')}</p> <p class="font-medium text-primary">{$t('filename')}</p>
<ul> <ul>
<li>{`{{filename}}`} - IMG_123</li> <li>{`{{filename}}`} - IMG_123</li>
<li>{`{{ext}}`} - jpg</li> <li>{`{{ext}}`} - jpg</li>
@@ -17,14 +17,14 @@
</div> </div>
<div> <div>
<p class="uppercase font-medium text-primary">{$t('filetype')}</p> <p class="font-medium text-primary">{$t('filetype')}</p>
<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('other')}</p> <p class="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>

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,
@@ -163,7 +163,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
@@ -266,10 +266,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}
@@ -496,7 +496,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" />
@@ -513,7 +513,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={resolve(`${AppRoute.ALBUMS}/${album.id}`)}> <a href={resolve(`${AppRoute.ALBUMS}/${album.id}`)}>
<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

@@ -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-sm" color="muted" 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 class="font-semibold">{$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="font-semibold mb-2">{$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="font-semibold mb-2">{$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,7 +20,8 @@
<div id="display-options-selection"> <div id="display-options-selection">
<fieldset> <fieldset>
<legend class="uppercase immich-form-label">{$t('display_options')}</legend> <Text class="font-semibold mb-2">{$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} />

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 class="font-semibold">{$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="font-semibold mb-2">{$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="font-semibold py-3">{$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,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 {
@@ -12,7 +13,7 @@
</script> </script>
<fieldset> <fieldset>
<legend class="immich-form-label">{$t('search_type')}</legend> <Text class="font-semibold py-3">{$t('search_type')}</Text>
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1 mb-2"> <div class="flex flex-wrap gap-x-5 gap-y-2 mt-1 mb-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" />
@@ -38,46 +39,47 @@
</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')} class="text-sm" for="context-input">
<input <Input
class="immich-form-input hover:cursor-text w-full mt-1!" type="text"
type="text" id="context-input"
id="context-input" name="context"
name="context" placeholder={$t('sunrise_on_the_beach')}
placeholder={$t('sunrise_on_the_beach')} bind:value={query}
bind:value={query} aria-labelledby="context-label"
/> />
</Field>
{:else if queryType === 'metadata'} {:else if queryType === 'metadata'}
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label> <Field label={$t('search_by_filename')} class="text-sm" for="file-name-input">
<input <Input
class="immich-form-input hover:cursor-text w-full mt-1!" type="text"
type="text" id="file-name-input"
id="file-name-input" name="context"
name="file-name" placeholder={$t('search_by_filename_example')}
placeholder={$t('search_by_filename_example')} bind:value={query}
bind:value={query} aria-labelledby="file-name-label"
aria-labelledby="file-name-label" />
/> </Field>
{:else if queryType === 'description'} {:else if queryType === 'description'}
<label for="description-input" class="immich-form-label">{$t('search_by_description')}</label> <Field label={$t('search_by_description')} class="text-sm" for="description">
<input <Input
class="immich-form-input hover:cursor-text w-full mt-1!" type="text"
type="text" id="description-input"
id="description-input" name="description"
name="description" placeholder={$t('search_by_description_example')}
placeholder={$t('search_by_description_example')} bind:value={query}
bind:value={query} aria-labelledby="description-label"
aria-labelledby="description-label" />
/> </Field>
{:else if queryType === 'ocr'} {:else if queryType === 'ocr'}
<label for="ocr-input" class="immich-form-label">{$t('search_by_ocr')}</label> <Field label={$t('search_by_ocr')} class="text-sm" for="ocr-input">
<input <Input
class="immich-form-input hover:cursor-text w-full mt-1!" type="text"
type="text" id="ocr-input"
id="ocr-input" name="ocr"
name="ocr" placeholder={$t('search_by_ocr_example')}
placeholder={$t('search_by_ocr_example')} bind:value={query}
bind:value={query} aria-labelledby="ocr-label"
aria-labelledby="ocr-label" />
/> </Field>
{/if} {/if}

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.