chore: tailwind linting (#28165)

chore: tailwind cannonical classes
pull/27820/merge
Daniel Dietzler 2026-05-01 06:18:03 +02:00 committed by GitHub
parent b60e9c6771
commit 5e9bda7fab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
248 changed files with 963 additions and 880 deletions

View File

@ -26,7 +26,8 @@
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",

View File

@ -304,7 +304,7 @@ test.describe('Timeline', () => {
await page.keyboard.down('Shift');
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
await expect(
thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'),
thumbnailUtils.locator(page).locator('.absolute.top-0.size-full.bg-immich-primary.opacity-40'),
).toHaveCount(3);
await thumbnailUtils.selectButton(page, assets[2].id).click();
await page.keyboard.up('Shift');

View File

@ -875,7 +875,7 @@ importers:
specifier: 7.0.0
version: 7.0.0(svelte@5.55.2)(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@tailwindcss/vite':
specifier: ^4.2.2
specifier: ^4.2.4
version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@testing-library/jest-dom':
specifier: ^6.4.2
@ -919,6 +919,9 @@ importers:
eslint-config-prettier:
specifier: ^10.1.8
version: 10.1.8(eslint@10.2.1(jiti@2.6.1))
eslint-plugin-better-tailwindcss:
specifier: ^4.5.0
version: 4.5.0(eslint@10.2.1(jiti@2.6.1))(tailwindcss@4.2.4)(typescript@6.0.3)
eslint-plugin-compat:
specifier: ^7.0.0
version: 7.0.1(eslint@10.2.1(jiti@2.6.1))
@ -956,7 +959,7 @@ importers:
specifier: ^1.3.3
version: 1.6.0(svelte@5.55.2)
tailwindcss:
specifier: ^4.2.2
specifier: ^4.2.4
version: 4.2.4
typescript:
specifier: ^6.0.0
@ -2730,6 +2733,10 @@ packages:
resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/css-tree@4.0.2':
resolution: {integrity: sha512-eqSkC3mka2tiqOuPZKqvxNJoRzpxMss3Np3Yqi4sW7nTTRCpTKB2hzrY4JRsi0ZP3QbVfp23sgEm7VCoOjesmw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/js@10.0.1':
resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
@ -5468,6 +5475,11 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@valibot/to-json-schema@1.6.0':
resolution: {integrity: sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A==}
peerDependencies:
valibot: ^1.3.0
'@vercel/oidc@3.0.5':
resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==}
engines: {node: '>= 20'}
@ -7329,6 +7341,19 @@ packages:
peerDependencies:
eslint: '>=7.0.0'
eslint-plugin-better-tailwindcss@4.5.0:
resolution: {integrity: sha512-EBNTx6OJYaWv7uUxHWTy1fhiNz2rZVkoeOHZzAJFwWaEPideBf04CMshrJ7YntG0KQzadlbRhHKYr32q5aBX4w==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0
oxlint: ^1.35.0
tailwindcss: ^3.3.0 || ^4.1.17
peerDependenciesMeta:
eslint:
optional: true
oxlint:
optional: true
eslint-plugin-compat@7.0.1:
resolution: {integrity: sha512-wDID2fVIAfxV9R1uSkCn5HscnNu8yMxDF1IaQGyD1C6XuWwJbuaDgMOSkVgOom0LzY8z0fXXXCy7AQQTERQUvQ==}
engines: {node: '>=18.x'}
@ -9052,6 +9077,9 @@ packages:
mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@ -11553,6 +11581,15 @@ packages:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tailwind-csstree@0.3.1:
resolution: {integrity: sha512-v147gLOR+E+9H4dNaP9rBeS/S/CTQJMRItlX9jLOXjdBGfSRauLwiz7LBCViaQmn6URXIlOdN6iMzSzOaeoUUw==}
engines: {node: '>=18.18'}
peerDependencies:
'@eslint/css': '>=1.0.0'
peerDependenciesMeta:
'@eslint/css':
optional: true
tailwind-merge@3.5.0:
resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==}
@ -12099,6 +12136,14 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
valibot@1.3.1:
resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==}
peerDependencies:
typescript: '>=5'
peerDependenciesMeta:
typescript:
optional: true
validator@13.15.35:
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
engines: {node: '>= 0.10'}
@ -15090,6 +15135,11 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/css-tree@4.0.2':
dependencies:
mdn-data: 2.27.1
source-map-js: 1.2.1
'@eslint/js@10.0.1(eslint@10.2.1(jiti@2.6.1))':
optionalDependencies:
eslint: 10.2.1(jiti@2.6.1)
@ -17887,6 +17937,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
'@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@6.0.3))':
dependencies:
valibot: 1.3.1(typescript@6.0.3)
'@vercel/oidc@3.0.5': {}
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.9.0)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
@ -17920,7 +17974,7 @@ snapshots:
obug: 2.1.1
std-env: 4.1.0
tinyrainbow: 3.1.0
vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/coverage-v8@4.1.5)(happy-dom@20.9.0)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.10(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
vitest: 4.1.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.5)(happy-dom@20.9.0)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.10(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.99.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
'@vitest/expect@3.2.4':
dependencies:
@ -19977,6 +20031,23 @@ snapshots:
dependencies:
eslint: 10.2.1(jiti@2.6.1)
eslint-plugin-better-tailwindcss@4.5.0(eslint@10.2.1(jiti@2.6.1))(tailwindcss@4.2.4)(typescript@6.0.3):
dependencies:
'@eslint/css-tree': 4.0.2
'@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@6.0.3))
enhanced-resolve: 5.21.0
jiti: 2.6.1
synckit: 0.11.12
tailwind-csstree: 0.3.1
tailwindcss: 4.2.4
tsconfig-paths-webpack-plugin: 4.2.0
valibot: 1.3.1(typescript@6.0.3)
optionalDependencies:
eslint: 10.2.1(jiti@2.6.1)
transitivePeerDependencies:
- '@eslint/css'
- typescript
eslint-plugin-compat@7.0.1(eslint@10.2.1(jiti@2.6.1)):
dependencies:
'@mdn/browser-compat-data': 6.1.5
@ -22094,6 +22165,8 @@ snapshots:
mdn-data@2.0.30: {}
mdn-data@2.27.1: {}
media-typer@0.3.0: {}
media-typer@1.1.0: {}
@ -25150,6 +25223,8 @@ snapshots:
tagged-tag@1.0.0: {}
tailwind-csstree@0.3.1: {}
tailwind-merge@3.5.0: {}
tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.4):
@ -25749,6 +25824,10 @@ snapshots:
uuid@8.3.2: {}
valibot@1.3.1(typescript@6.0.3):
optionalDependencies:
typescript: 6.0.3
validator@13.15.35: {}
value-equal@1.0.1: {}

View File

@ -1,6 +1,7 @@
import js from '@eslint/js';
import tslintPluginCompat from '@koddsson/eslint-plugin-tscompat';
import prettier from 'eslint-config-prettier';
import eslintPluginBetterTailwindcss from 'eslint-plugin-better-tailwindcss';
import eslintPluginCompat from 'eslint-plugin-compat';
import eslintPluginSvelte from 'eslint-plugin-svelte';
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
@ -18,7 +19,6 @@ export default typescriptEslint.config(
...eslintPluginSvelte.configs.recommended,
eslintPluginUnicorn.configs.recommended,
js.configs.recommended,
prettier,
{
plugins: {
tscompat: tslintPluginCompat,
@ -134,6 +134,18 @@ export default typescriptEslint.config(
},
},
{
extends: [eslintPluginBetterTailwindcss.configs.recommended],
settings: {
'better-tailwindcss': {
entryPoint: 'src/app.css',
},
},
rules: {
'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
'better-tailwindcss/no-unknown-classes': 'off',
},
files: ['**/*.svelte'],
languageOptions: {

View File

@ -76,7 +76,7 @@
"@sveltejs/enhanced-img": "^0.10.4",
"@sveltejs/kit": "^2.56.1",
"@sveltejs/vite-plugin-svelte": "7.0.0",
"@tailwindcss/vite": "^4.2.2",
"@tailwindcss/vite": "^4.2.4",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.8",
"@testing-library/user-event": "^14.5.2",
@ -91,6 +91,7 @@
"dotenv": "^17.0.0",
"eslint": "^10.2.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-better-tailwindcss": "^4.5.0",
"eslint-plugin-compat": "^7.0.0",
"eslint-plugin-svelte": "^3.12.4",
"eslint-plugin-unicorn": "^64.0.0",
@ -104,7 +105,7 @@
"svelte": "5.55.2",
"svelte-check": "^4.4.6",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.2.2",
"tailwindcss": "^4.2.4",
"typescript": "^6.0.0",
"typescript-eslint": "^8.45.0",
"vite": "^8.0.0",

View File

@ -1,8 +1,38 @@
@import 'tailwindcss';
@import '@immich/ui/theme/default.css';
@source "../node_modules/@immich/ui";
/* @import '../../../ui/packages/ui/dist/theme/default.css'; */
@custom-variant dark (&:where(.dark, .dark *):not(.light));
@theme inline {
--color-immich-primary: rgb(var(--immich-primary));
--color-immich-bg: rgb(var(--immich-bg));
--color-immich-fg: rgb(var(--immich-fg));
--color-immich-gray: rgb(var(--immich-gray));
--color-immich-dark-primary: rgb(var(--immich-dark-primary));
--color-immich-dark-bg: rgb(var(--immich-dark-bg));
--color-immich-dark-fg: rgb(var(--immich-dark-fg));
--color-immich-dark-gray: rgb(var(--immich-dark-gray));
}
@theme {
--font-sans: 'GoogleSans', sans-serif;
--font-mono: 'GoogleSansCode', monospace;
--spacing-18: 4.5rem;
--breakpoint-tall: 800px;
--breakpoint-2xl: 1535px;
--breakpoint-xl: 1279px;
--breakpoint-lg: 1023px;
--breakpoint-md: 767px;
--breakpoint-sm: 639px;
--breakpoint-sidebar: 850px;
}
@utility immich-form-input {
@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;
}
@ -34,35 +64,6 @@
grid-template-columns: repeat(auto-fill, minmax(min(calc(var(--spacing) * --value(number)), 100%), 1fr));
}
@custom-variant dark (&:where(.dark, .dark *):not(.light));
@theme inline {
--color-immich-primary: rgb(var(--immich-primary));
--color-immich-bg: rgb(var(--immich-bg));
--color-immich-fg: rgb(var(--immich-fg));
--color-immich-gray: rgb(var(--immich-gray));
--color-immich-dark-primary: rgb(var(--immich-dark-primary));
--color-immich-dark-bg: rgb(var(--immich-dark-bg));
--color-immich-dark-fg: rgb(var(--immich-dark-fg));
--color-immich-dark-gray: rgb(var(--immich-dark-gray));
}
@theme {
--font-sans: 'GoogleSans', sans-serif;
--font-mono: 'GoogleSansCode', monospace;
--spacing-18: 4.5rem;
--breakpoint-tall: 800px;
--breakpoint-2xl: 1535px;
--breakpoint-xl: 1279px;
--breakpoint-lg: 1023px;
--breakpoint-md: 767px;
--breakpoint-sm: 639px;
--breakpoint-sidebar: 850px;
}
@layer base {
:root {
/* light */
@ -168,7 +169,7 @@
.maplibregl-popup {
.maplibregl-popup-tip {
@apply border-t-subtle! translate-y-[-1px];
@apply border-t-subtle! -translate-y-px;
}
.maplibregl-popup-content {

View File

@ -148,11 +148,11 @@
});
</script>
<div class="relative h-full w-full overflow-hidden" bind:this={ref}>
<div class="relative size-full overflow-hidden" bind:this={ref}>
{@render backdrop?.()}
<div
class="absolute inset-0 pointer-events-none"
class="pointer-events-none absolute inset-0"
style:inset-inline-start={insetInlineStart}
style:top
style:width
@ -165,7 +165,7 @@
{#if show.thumbhash}
{#if asset.thumbhash}
<!-- Thumbhash / spinner layer -->
<Thumbhash base64ThumbHash={asset.thumbhash} class="h-full w-full absolute" />
<Thumbhash base64ThumbHash={asset.thumbhash} class="absolute size-full" />
{:else if show.spinner}
<DelayedLoadingSpinner />
{/if}
@ -185,7 +185,7 @@
{/if}
{#if show.brokenAsset}
<BrokenAsset class="text-xl h-full w-full absolute" />
<BrokenAsset class="absolute size-full text-xl" />
{/if}
{#if show.preview}

View File

@ -15,7 +15,7 @@
<Card color="secondary">
<CardHeader>
<div class="flex w-full justify-between items-center px-4 py-2">
<div class="flex w-full items-center justify-between px-4 py-2">
<div class="flex gap-2 text-primary">
<Icon {icon} size="1.5rem" />
<CardTitle>{title}</CardTitle>

View File

@ -50,7 +50,7 @@
</script>
<Label label={$t('permission')} for="permission-container" />
<div class="flex items-center gap-2 m-4" id="permission-container">
<div class="m-4 flex items-center gap-2" id="permission-container">
<Checkbox id="input-select-all" size="tiny" checked={allItemsSelected} onCheckedChange={onCheckedAllChange} />
<Label label={$t('select_all')} for="input-select-all" />
</div>

View File

@ -30,8 +30,8 @@
);
</script>
<div class="h-full flex flex-col">
<div class="flex h-16 w-full justify-between items-center border-b py-2 px-4 md:px-2">
<div class="flex h-full flex-col">
<div class="flex h-16 w-full items-center justify-between border-b px-4 py-2 md:px-2">
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
{#if enabledActions.length > 0}

View File

@ -36,7 +36,7 @@
onLoad={() => adaptiveImageLoader.onLoad(quality)}
onError={() => adaptiveImageLoader.onError(quality)}
bind:ref
class="h-full w-full bg-transparent pointer-events-auto"
class="pointer-events-auto size-full bg-transparent"
{alt}
{role}
draggable={false}

View File

@ -58,7 +58,7 @@
<DatePicker bind:value={getSelectedDate, setSelectedDate} />
</Field>
<div class="flex flex-wrap gap-2 mt-2">
<div class="mt-2 flex flex-wrap gap-2">
{#each expiredDateOptions as option (option.value)}
<Button
size="tiny"

View File

@ -30,7 +30,7 @@
});
</script>
<div class="flex flex-col gap-4 mt-4">
<div class="mt-4 flex flex-col gap-4">
<div>
<Field label={$t('custom_url')} description={$t('shared_link_custom_url_description')}>
<Input bind:value={slug} autocomplete="off" />

View File

@ -106,7 +106,7 @@
});
</script>
<section class="dark:text-immich-dark-fg mt-2">
<section class="mt-2 dark:text-immich-dark-fg">
<div in:fade={{ duration }} class="mx-4 flex flex-col gap-4 py-4">
<p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.storage_template_more_details">
@ -164,7 +164,7 @@
<SupportedVariablesPanel />
</section>
<div class="flex flex-col mt-2">
<div class="mt-2 flex flex-col">
<!-- <h3 class="text-base font-medium text-primary">{$t('template')}</h3> -->
<Heading size="tiny" color="primary">
{$t('template')}
@ -199,20 +199,20 @@
</FormatMessage>
</p>
<p class="p-4 py-2 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
<p class="mt-2 rounded-lg bg-gray-200 p-4 py-2 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
>UPLOAD_LOCATION/library/{authManager.user.storageLabel || authManager.user.id}</span
>/{parsedTemplate()}.jpg
</p>
<form autocomplete="off" class="flex flex-col" onsubmit={preventDefault(bubble('submit'))}>
<div class="flex flex-col my-2">
<div class="my-2 flex flex-col">
{#if templateOptions}
<label class="font-medium text-primary text-sm" for="preset-select">
<label class="text-sm font-medium text-primary" for="preset-select">
{$t('preset')}
</label>
<select
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
class="mt-2 immich-form-input rounded-lg bg-slate-200 p-2 text-sm hover:cursor-pointer dark:bg-gray-600"
disabled={disabled || !configToEdit.storageTemplate.enabled}
name="presets"
id="preset-select"

View File

@ -27,7 +27,7 @@
<Text size="small">{$t('date_and_time')}</Text>
<Card class="mt-2 text-sm bg-light-50 shadow-none">
<Card class="mt-2 bg-light-50 text-sm shadow-none">
<CardHeader>
<Text class="mb-1">{$t('admin.storage_template_date_time_description')}</Text>
<Text color="primary"

View File

@ -5,7 +5,7 @@
<Text size="small">{$t('other_variables')}</Text>
<Card class="mt-2 text-sm bg-light-50 shadow-none">
<Card class="mt-2 bg-light-50 text-sm shadow-none">
<CardBody>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div>

View File

@ -34,13 +34,13 @@
</script>
<div
class="group relative rounded-2xl border border-transparent p-5 hover:bg-gray-100 hover:border-gray-200 dark:hover:border-gray-800 dark:hover:bg-gray-900"
class="group relative rounded-2xl border border-transparent p-5 hover:border-gray-200 hover:bg-gray-100 dark:hover:border-gray-800 dark:hover:bg-gray-900"
data-testid="album-card"
>
{#if onShowContextMenu}
<div
id="icon-{album.id}"
class="absolute end-6 top-6 opacity-0 group-hover:opacity-100 focus-within:opacity-100"
class="absolute inset-e-6 top-6 opacity-0 group-hover:opacity-100 focus-within:opacity-100"
data-testid="context-button-parent"
>
<IconButton
@ -60,7 +60,7 @@
<div class="mt-4">
<p
class="w-full leading-6 text-lg line-clamp-2 font-semibold text-black dark:text-white group-hover:text-primary"
class="line-clamp-2 w-full text-lg/6 font-semibold text-black group-hover:text-primary dark:text-white"
data-testid="album-name"
title={album.albumName}
>
@ -68,7 +68,7 @@
</p>
{#if showDateRange && album.startDate && album.endDate}
<p class="flex text-sm dark:text-immich-dark-fg capitalize">
<p class="flex text-sm capitalize dark:text-immich-dark-fg">
{getShortDateRange(album.startDate, album.endDate)}
</p>
{/if}

View File

@ -48,11 +48,11 @@
<button
type="button"
onclick={() => toggleAlbumGroupCollapsing(group.id)}
class="w-full text-start mt-2 pt-2 pe-2 pb-2 rounded-md transition-colors cursor-pointer dark:text-immich-dark-fg hover:text-primary hover:bg-subtle dark:hover:bg-immich-dark-gray"
class="mt-2 w-full cursor-pointer rounded-md py-2 pe-2 text-start transition-colors hover:bg-subtle hover:text-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray"
aria-expanded={!isCollapsed}
>
<Icon icon={mdiChevronRight} size="24" class="inline-block -mt-2.5 transition-all duration-250 {iconRotation}" />
<span class="font-bold text-3xl text-black dark:text-white">{group.name}</span>
<Icon icon={mdiChevronRight} size="24" class="-mt-2.5 inline-block transition-all duration-250 {iconRotation}" />
<span class="text-3xl font-bold text-black dark:text-white">{group.name}</span>
<span class="ms-1.5">({$t('albums_count', { values: { count: albums.length } })})</span>
</button>
<hr class="dark:border-immich-dark-gray" />

View File

@ -34,7 +34,7 @@
const { ViewQrCode, Copy, Delete } = $derived(getSharedLinkActions($t, sharedLink));
</script>
<div class="flex justify-between items-center">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<Text size="small">{sharedLink.description || album.albumName}</Text>
<Text size="tiny" color="muted">{getShareProperties()}</Text>

View File

@ -70,11 +70,11 @@
}}
/>
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
<main class="relative h-dvh overflow-hidden px-2 pt-(--navbar-height) max-md:pt-(--navbar-height-md) md:px-6">
<Timeline enableRouting={true} {album} bind:timelineManager {options} assetInteraction={assetMultiSelectManager}>
<section class="pt-8 md:pt-24 px-2 md:px-0">
<section class="px-2 pt-8 md:px-0 md:pt-24">
<!-- ALBUM TITLE -->
<h1 class="text-2xl md:text-4xl lg:text-6xl text-primary outline-none transition-all">
<h1 class="text-2xl text-primary transition-all outline-none md:text-4xl lg:text-6xl">
{album.albumName}
</h1>
@ -85,7 +85,7 @@
<!-- ALBUM DESCRIPTION -->
{#if album.description}
<p
class="whitespace-pre-line mb-12 mt-6 w-full pb-2 text-start font-medium text-base text-black dark:text-gray-300"
class="mt-6 mb-12 w-full pb-2 text-start text-base font-medium whitespace-pre-line text-black dark:text-gray-300"
>
{album.description}
</p>

View File

@ -45,20 +45,20 @@
{@const isCollapsed = isAlbumGroupCollapsed($albumViewSettings, albumGroup.id)}
{@const iconRotation = isCollapsed ? 'rotate-0' : 'rotate-90'}
<tbody
class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg mt-4"
class="mt-4 block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
<tr
class="flex w-full place-items-center p-2 md:ps-5 md:pe-5 md:pt-3 md:pb-3"
class="flex w-full place-items-center p-2 md:py-3 md:ps-5 md:pe-5"
onclick={() => toggleAlbumGroupCollapsing(albumGroup.id)}
aria-expanded={!isCollapsed}
>
<td class="text-md text-start -mb-1">
<td class="text-md -mb-1 text-start">
<Icon
icon={mdiChevronRight}
size="20"
class="inline-block -mt-2 transition-all duration-250 {iconRotation}"
class="-mt-2 inline-block transition-all duration-250 {iconRotation}"
/>
<span class="font-bold text-2xl">{albumGroup.name}</span>
<span class="text-2xl font-bold">{albumGroup.name}</span>
<span class="ms-1.5">
({$t('albums_count', { values: { count: albumGroup.albums.length } })})
</span>
@ -67,7 +67,7 @@
</tbody>
{#if !isCollapsed}
<tbody
class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg mt-4"
class="mt-4 block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray dark:text-immich-dark-fg"
transition:slide={{ duration: 300 }}
>
{#each albumGroup.albums as album (album.id)}

View File

@ -32,17 +32,17 @@
</script>
<tr
class="flex w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:px-5 md:py-2"
class="flex w-full place-items-center border-3 border-transparent p-2 text-center odd:bg-subtle/80 even:bg-subtle/20 hover:cursor-pointer hover:border-immich-primary/75 md:px-5 md:py-2 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75"
onclick={() => goto(Route.viewAlbum(album))}
{oncontextmenu}
>
<td class="text-md text-ellipsis text-start w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
<td class="text-md w-8/12 items-center text-start text-ellipsis sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]">
{album.albumName}
{#if album.shared}
<Icon
icon={mdiShareVariantOutline}
size="16"
class="inline ms-1 opacity-70"
class="ms-1 inline opacity-70"
title={album.albumUsers.find(({ user: { id } }) => id === authManager.user.id)?.role === AlbumUserRole.Owner
? $t('shared_by_you')
: $t('shared_by_user', {
@ -51,23 +51,23 @@
/>
{/if}
</td>
<td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
<td class="text-md text-center text-ellipsis sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
{$t('items_count', { values: { count: album.assetCount } })}
</td>
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]">
<td class="text-md hidden w-3/12 text-center text-ellipsis sm:block xl:w-[15%] 2xl:w-[12%]">
{dateLocaleString(album.updatedAt)}
</td>
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]">
<td class="text-md hidden w-3/12 text-center text-ellipsis sm:block xl:w-[15%] 2xl:w-[12%]">
{dateLocaleString(album.createdAt)}
</td>
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]">
<td class="text-md hidden text-center text-ellipsis xl:block xl:w-[15%] 2xl:w-[12%]">
{#if album.endDate}
{dateLocaleString(album.endDate)}
{:else}
-
{/if}
</td>
<td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]">
<td class="text-md hidden text-center text-ellipsis xl:block xl:w-[15%] 2xl:w-[12%]">
{#if album.startDate}
{dateLocaleString(album.startDate)}
{:else}

View File

@ -16,7 +16,7 @@
let { isLiked, numberOfComments, numberOfLikes, disabled, onFavorite }: Props = $props();
</script>
<div class="flex p-1 items-center justify-center rounded-full gap-1 bg-subtle/70 border">
<div class="flex items-center justify-center gap-1 rounded-full border bg-subtle/70 p-1">
<Button
{disabled}
onclick={onFavorite}

View File

@ -110,9 +110,9 @@
};
</script>
<div class="overflow-y-hidden relative h-full border-l border-subtle bg-subtle" bind:offsetHeight={innerHeight}>
<div class="w-full h-full">
<div class="flex w-full h-fit dark:text-immich-dark-fg p-2 bg-subtle" bind:clientHeight={activityHeight}>
<div class="relative h-full overflow-y-hidden border-l border-subtle bg-subtle" bind:offsetHeight={innerHeight}>
<div class="size-full">
<div class="flex h-fit w-full bg-subtle p-2 dark:text-immich-dark-fg" bind:clientHeight={activityHeight}>
<div class="flex place-items-center gap-2">
<IconButton
shape="round"
@ -128,21 +128,21 @@
</div>
{#if innerHeight}
<div
class="overflow-y-auto immich-scrollbar relative w-full px-2"
class="relative w-full overflow-y-auto px-2 immich-scrollbar"
style="height: {divHeight}px;padding-bottom: {chatHeight}px"
>
{#each activityManager.activities as reaction, index (reaction.id)}
{#if reaction.type === ReactionType.Comment}
<div class="flex dark:bg-gray-800 bg-gray-200 py-3 ps-3 mt-3 rounded-lg gap-4 justify-start">
<div class="mt-3 flex justify-start gap-4 rounded-lg bg-gray-200 py-3 ps-3 dark:bg-gray-800">
<div class="flex items-center">
<UserAvatar user={reaction.user} size="sm" />
</div>
<div class="w-full leading-4 overflow-hidden self-center wrap-break-word text-sm">{reaction.comment}</div>
<div class="w-full self-center overflow-hidden text-sm/4 wrap-break-word">{reaction.comment}</div>
{#if assetId === undefined && reaction.assetId}
<a class="aspect-square w-19 h-19" href={Route.viewAlbumAsset({ albumId, assetId: reaction.assetId })}>
<a class="aspect-square size-19" href={Route.viewAlbumAsset({ albumId, assetId: reaction.assetId })}>
<img
class="rounded-lg w-19 h-19 object-cover"
class="size-19 rounded-lg object-cover"
src={getAssetMediaUrl({ id: reaction.assetId })}
alt="Profile picture of {reaction.user.name}, who commented on this asset"
/>
@ -170,7 +170,7 @@
{#if (index != activityManager.activities.length - 1 && !shouldGroup(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
<div
class="pt-1 px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
class="w-full px-2 pt-1 text-right text-sm text-gray-500 dark:text-gray-300"
title={new Date(reaction.createdAt).toLocaleDateString(undefined, timeOptions)}
>
{timeSince(luxon.DateTime.fromISO(reaction.createdAt, { locale: $locale }))}
@ -178,7 +178,7 @@
{/if}
{:else if reaction.type === ReactionType.Like}
<div class="relative">
<div class="flex py-3 ps-3 mt-3 gap-4 items-center text-sm">
<div class="mt-3 flex items-center gap-4 py-3 ps-3 text-sm">
<div class="text-primary"><Icon icon={mdiThumbUp} size="20" /></div>
<div class="w-full" title={`${reaction.user.name} (${reaction.user.email})`}>
@ -190,12 +190,9 @@
})}
</div>
{#if assetId === undefined && reaction.assetId}
<a
class="aspect-square w-19 h-19"
href={Route.viewAlbumAsset({ albumId, assetId: reaction.assetId })}
>
<a class="aspect-square size-19" href={Route.viewAlbumAsset({ albumId, assetId: reaction.assetId })}>
<img
class="rounded-lg w-19 h-19 object-cover"
class="size-19 rounded-lg object-cover"
src={getAssetMediaUrl({ id: reaction.assetId })}
alt="Profile picture of {reaction.user.name}, who liked this asset"
/>
@ -222,7 +219,7 @@
</div>
{#if (index != activityManager.activities.length - 1 && isTenMinutesApart(activityManager.activities[index].createdAt, activityManager.activities[index + 1].createdAt)) || index === activityManager.activities.length - 1}
<div
class="pt-1 px-2 text-right w-full text-sm text-gray-500 dark:text-gray-300"
class="w-full px-2 pt-1 text-right text-sm text-gray-500 dark:text-gray-300"
title={new Date(reaction.createdAt).toLocaleDateString(navigator.language, timeOptions)}
>
{timeSince(luxon.DateTime.fromISO(reaction.createdAt, { locale: $locale }))}
@ -235,13 +232,13 @@
{/if}
</div>
<div class="absolute w-full bottom-0">
<div class="absolute bottom-0 w-full">
<div class="flex items-center justify-center p-2" bind:clientHeight={chatHeight}>
<div class="flex p-2 gap-4 h-fit bg-gray-200 text-immich-dark-gray rounded-3xl w-full">
<div class="flex h-fit w-full gap-4 rounded-3xl bg-gray-200 p-2 text-immich-dark-gray">
<div>
<UserAvatar user={authManager.user} size="md" noTitle />
</div>
<form class="flex w-full items-center max-h-56 gap-1" {onsubmit}>
<form class="flex max-h-56 w-full items-center gap-1" {onsubmit}>
<Textarea
{disabled}
bind:value={message}
@ -254,16 +251,16 @@
}))}
class="{disabled
? 'cursor-not-allowed'
: ''} ring-0! w-full max-h-56 pe-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200 dark:bg-gray-200"
: ''} max-h-56 w-full resize-none items-center overflow-y-auto bg-gray-200 pe-2 leading-4 ring-0! outline-none dark:bg-gray-200"
/>
{#if isSendingMessage}
<div class="flex place-items-center pb-2 ms-0">
<div class="ms-0 flex place-items-center pb-2">
<div class="flex w-full place-items-center">
<LoadingSpinner size="large" />
</div>
</div>
{:else if message}
<div class="flex items-center w-fit ms-0 light">
<div class="light ms-0 flex w-fit items-center">
<IconButton
shape="round"
aria-label={$t('send_message')}

View File

@ -116,7 +116,7 @@
<div
role="group"
class={[
'relative flex w-full text-start justify-between transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl my-2 hover:cursor-pointer',
'relative my-2 flex w-full justify-between rounded-xl text-start transition-colors hover:cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700',
{ 'bg-primary/10 hover:bg-primary/10': multiSelected },
]}
onmouseenter={onMouseEnter}
@ -126,26 +126,24 @@
type="button"
onclick={onAlbumClick}
use:scrollIntoViewIfSelected
class="flex w-full gap-4 px-2 py-2 text-start"
class="flex w-full gap-4 p-2 text-start"
class:bg-gray-200={selected}
class:dark:bg-gray-700={selected}
use:longPress={{ onLongPress: () => handleMultiSelectClicked() }}
>
<span class="h-16 w-16 shrink-0 rounded-xl bg-slate-300">
<span class="size-16 shrink-0 rounded-xl bg-slate-300">
{#if album.albumThumbnailAssetId}
<img
src={getAssetMediaUrl({ id: album.albumThumbnailAssetId })}
alt={album.albumName}
class={['h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg']}
class={['size-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg']}
data-testid="album-image"
draggable="false"
/>
{/if}
</span>
<span class="flex h-full flex-col items-start justify-center overflow-hidden">
<span class="w-full shrink overflow-hidden text-ellipsis whitespace-nowrap"
>{albumNameArray[0]}<b>{albumNameArray[1]}</b>{albumNameArray[2]}</span
>
<span class="w-full shrink truncate">{albumNameArray[0]}<b>{albumNameArray[1]}</b>{albumNameArray[2]}</span>
<span class="flex gap-1 text-sm">
<AlbumListItemDetails {album} />
</span>
@ -156,7 +154,7 @@
<button
type="button"
onclick={handleMultiSelectClicked}
class="absolute right-0 top-4 p-3 focus:outline-none hover:cursor-pointer"
class="absolute top-4 right-0 p-3 hover:cursor-pointer focus:outline-none"
role="checkbox"
tabindex={-1}
aria-checked={selected}

View File

@ -457,7 +457,7 @@
<section
id="immich-asset-viewer"
class="fixed start-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
class="fixed inset-s-0 top-0 grid size-full grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
use:focusTrap
bind:this={assetViewerHtmlElement}
>
@ -496,13 +496,13 @@
{/if}
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !assetViewerManager.isFaceEditMode && previousAsset}
<div class="my-auto col-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
<div class="col-span-1 col-start-1 row-span-full row-start-1 my-auto justify-self-start">
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
</div>
{/if}
<!-- Asset Viewer -->
<div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
<div data-viewer-content class="relative z-[-1] col-span-4 col-start-1 row-span-full row-start-1">
{#if viewerKind === 'StackVideoViewer'}
<VideoViewer
asset={previewStackedAsset!}
@ -550,7 +550,7 @@
{/if}
{#if showActivityStatus}
<div class="absolute bottom-0 end-0 mb-20 me-8">
<div class="absolute inset-e-0 bottom-0 me-8 mb-20">
<ActivityStatus
disabled={!album?.isActivityEnabled}
isLiked={activityManager.isLiked}
@ -562,14 +562,14 @@
{/if}
{#if showOcrButton}
<div class="absolute bottom-0 end-0 mb-6 me-6 drop-shadow-[0_0_1px_rgba(0,0,0,0.4)]">
<div class="absolute inset-e-0 bottom-0 me-6 mb-6 drop-shadow-[0_0_1px_rgba(0,0,0,0.4)]">
<OcrButton />
</div>
{/if}
</div>
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && !assetViewerManager.isFaceEditMode && nextAsset}
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
<div class="col-span-1 col-start-4 row-span-full row-start-1 my-auto justify-self-end">
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
</div>
{/if}
@ -579,7 +579,7 @@
transition:fly={{ duration: 150 }}
id="detail-panel"
class={[
'row-start-1 row-span-4 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light',
'row-span-4 row-start-1 overflow-y-auto bg-light transition-all dark:border-l dark:border-s-immich-dark-gray',
showDetailPanel ? 'w-90' : 'w-100',
]}
translate="yes"
@ -594,11 +594,11 @@
{#if stack && withStacked && !assetViewerManager.isShowEditor}
{@const stackedAssets = stack.assets}
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none">
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar">
<div id="stack-slideshow" class="pointer-events-none absolute bottom-0 col-span-4 col-start-1 w-full">
<div class="no-wrap horizontal-scrollbar relative flex flex-row overflow-x-auto overflow-y-hidden">
{#each stackedAssets as stackedAsset (stackedAsset.id)}
<div
class={['inline-block px-1 relative transition-all pb-2 pointer-events-auto']}
class={['pointer-events-auto relative inline-block px-1 pb-2 transition-all']}
style:bottom={stackedAsset.id === asset.id ? '0' : '-10px'}
>
<Thumbnail
@ -618,8 +618,8 @@
/>
{#if stackedAsset.id === asset.id}
<div class="w-full flex place-items-center place-content-center">
<div class="w-2 h-2 bg-white rounded-full flex mt-0.5"></div>
<div class="flex w-full place-content-center place-items-center">
<div class="mt-0.5 flex size-2 rounded-full bg-white"></div>
</div>
{/if}
</div>
@ -632,7 +632,7 @@
<div
transition:fly={{ duration: 150 }}
id="activity-panel"
class="row-start-1 row-span-5 w-90 md:w-115 overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray"
class="row-span-5 row-start-1 w-90 overflow-y-auto transition-all md:w-115 dark:border-l dark:border-s-immich-dark-gray"
translate="yes"
>
<ActivityViewer

View File

@ -103,14 +103,14 @@
<CommandPaletteDefaultProvider name={$t('assets')} actions={withoutIcons([Close, Cast, ...Object.values(Actions)])} />
<div
class="flex h-16 place-items-center justify-between bg-linear-to-b from-black/40 px-3 transition-transform duration-200 drop-shadow-[0_0_1px_rgba(0,0,0,0.4)]"
class="flex h-16 place-items-center justify-between bg-linear-to-b from-black/40 px-3 drop-shadow-[0_0_1px_rgba(0,0,0,0.4)] transition-transform duration-200"
>
<div class="dark">
<ActionButton action={Close} />
</div>
<div
class="flex p-1 -m-1 items-center gap-2 overflow-x-auto *:shrink-0 dark"
class="dark -m-1 flex items-center gap-2 overflow-x-auto p-1 *:shrink-0"
data-testid="asset-viewer-navbar-actions"
>
{#if assetViewerManager.isImageLoading}

View File

@ -126,7 +126,7 @@
</div>
{#if asset.isOffline}
<section class="px-4 py-4">
<section class="p-4">
<div role="alert">
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">
{$t('asset_offline')}
@ -140,7 +140,7 @@
{/if}
</p>
</div>
<div class="rounded-b bg-red-500 px-4 py-2 text-white text-sm">
<div class="rounded-b bg-red-500 px-4 py-2 text-sm text-white">
<p>{asset.originalPath}</p>
</div>
</div>
@ -151,7 +151,7 @@
<DetailPanelRating {asset} {isOwner} />
<DetailPanelPeople {asset} {isOwner} {previousRoute} />
<div class="px-4 py-4">
<div class="p-4">
{#if asset.exifInfo}
<div class="flex h-10 w-full items-center justify-between text-sm">
<Text size="small" color="muted">{$t('details')}</Text>
@ -166,7 +166,7 @@
<div><Icon icon={mdiImageOutline} size="24" /></div>
<div>
<p class="break-all flex place-items-center gap-2 whitespace-pre-wrap">
<p class="flex place-items-center gap-2 break-all whitespace-pre-wrap">
{asset.originalFileName}
{#if isOwner}
<IconButton
@ -181,7 +181,7 @@
{/if}
</p>
{#if assetViewerManager.isShowAssetPath}
<p class="text-xs opacity-50 break-all pb-2 hover:text-primary" transition:slide={{ duration: 250 }}>
<p class="pb-2 text-xs break-all opacity-50 hover:text-primary" transition:slide={{ duration: 250 }}>
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve this is supposed to be treated as an absolute/external link -->
<a href={getAssetFolderHref(asset)} title={$t('go_to_folder')} class="whitespace-pre-wrap">
{asset.originalPath}
@ -251,7 +251,7 @@
<a
href={Route.search({ lensModel: asset.exifInfo.lensModel })}
title="{$t('search_for')} {asset.exifInfo.lensModel}"
class="hover:text-primary line-clamp-1"
class="line-clamp-1 hover:text-primary"
>
{asset.exifInfo.lensModel}
</a>
@ -280,7 +280,7 @@
{#await import('$lib/components/shared-components/map/Map.svelte')}
{#await delay(timeToLoadTheMap) then}
<!-- show the loading spinner only if loading the map takes too much time -->
<div class="flex items-center justify-center h-full w-full">
<div class="flex size-full items-center justify-center">
<LoadingSpinner />
</div>
{/await}
@ -323,14 +323,14 @@
{/if}
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
<section class="px-6 dark:text-immich-dark-fg mt-4">
<section class="mt-4 px-6 dark:text-immich-dark-fg">
<Text size="small" color="muted">{$t('shared_by')}</Text>
<div class="flex gap-4 pt-4">
<div>
<UserAvatar user={asset.owner} size="md" />
</div>
<div class="mb-auto mt-auto">
<div class="my-auto">
<p>
{asset.owner.name}
</p>
@ -341,24 +341,24 @@
{#await albums then albums}
{#if albums.length > 0}
<section class="px-6 py-6 dark:text-immich-dark-fg">
<section class="p-6 dark:text-immich-dark-fg">
<div class="pb-4">
<Text size="small" color="muted">{$t('appears_in')}</Text>
</div>
{#each albums as album (album.id)}
<a href={Route.viewAlbum(album)}>
<div class="flex gap-4 pt-2 hover:cursor-pointer items-center">
<div class="flex items-center gap-4 pt-2 hover:cursor-pointer">
<div>
<img
alt={album.albumName}
class="h-12.5 w-12.5 rounded object-cover"
class="size-12.5 rounded-sm object-cover"
src={album.albumThumbnailAssetId &&
getAssetMediaUrl({ id: album.albumThumbnailAssetId, size: AssetMediaSize.Preview })}
draggable="false"
/>
</div>
<div class="mb-auto mt-auto">
<div class="my-auto">
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
<div class="flex flex-col gap-0 text-sm">
<div>

View File

@ -38,7 +38,7 @@
{#if dateTime}
<button
type="button"
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
onclick={handleChangeDate}
title={isOwner ? $t('edit_date') : ''}
class:hover:text-primary={isOwner}
@ -75,7 +75,7 @@
{/if}
</button>
{:else if !dateTime && isOwner}
<div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex place-items-start justify-between gap-4 py-4">
<div class="flex gap-4">
<Icon icon={mdiCalendar} size="24" />
</div>

View File

@ -30,10 +30,10 @@
</script>
{#if isOwner}
<section class="px-4 mt-10">
<section class="mt-10 px-4">
<Textarea
bind:value={description}
class="max-h-40 pl-0 outline-none border-b border-gray-500 bg-transparent ring-0 focus:ring-0 resize-none focus:border-b-2 focus:border-immich-primary dark:focus:border-immich-dark-primary dark:bg-transparent"
class="max-h-40 resize-none border-b border-gray-500 bg-transparent pl-0 ring-0 outline-none focus:border-b-2 focus:border-immich-primary focus:ring-0 dark:bg-transparent dark:focus:border-immich-dark-primary"
rows={1}
grow
shape="rectangle"
@ -47,7 +47,7 @@
/>
</section>
{:else if description}
<section class="px-4 mt-6">
<p class="wrap-break-word whitespace-pre-line w-full text-black dark:text-white text-base">{description}</p>
<section class="mt-6 px-4">
<p class="w-full text-base wrap-break-word whitespace-pre-line text-black dark:text-white">{description}</p>
</section>
{/if}

View File

@ -33,7 +33,7 @@
{#if asset.exifInfo?.country}
<button
type="button"
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
onclick={isOwner ? onAction : undefined}
title={isOwner ? $t('edit_location') : ''}
class:hover:text-primary={isOwner}
@ -67,7 +67,7 @@
{:else if !asset.exifInfo?.city && isOwner}
<button
type="button"
class="flex w-full text-start justify-between place-items-start gap-4 py-4 rounded-lg hover:text-primary"
class="flex w-full place-items-start justify-between gap-4 rounded-lg py-4 text-start hover:text-primary"
onclick={onAction}
title={$t('add_location')}
>
@ -75,7 +75,7 @@
<div><Icon icon={mdiMapMarkerOutline} size="24" /></div>
<p>{$t('add_a_location')}</p>
</div>
<div class="focus:outline-none p-1">
<div class="p-1 focus:outline-none">
<Icon icon={mdiPencil} size="20" />
</div>
</button>

View File

@ -60,7 +60,7 @@
<section class="px-4 pt-4 text-sm">
<div class="flex h-10 w-full items-center justify-between">
<Text size="small" color="muted">{$t('people')}</Text>
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
{#if people.some((person) => person.isHidden)}
<IconButton
aria-label={$t('show_hidden_people')}
@ -118,7 +118,7 @@
widthStyle="100%"
hidden={person.isHidden}
highlighted={isHighlighted}
class="group-focus-visible:outline-2 outline-offset-2 outline-immich-primary dark:outline-immich-dark-primary"
class="outline-offset-2 outline-immich-primary group-focus-visible:outline-2 dark:outline-immich-dark-primary"
/>
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
{#if person.birthDate && person.formattedAge}

View File

@ -37,11 +37,11 @@
<OnEvents {onAssetsTag} />
{#if isOwner && !authManager.isSharedLink}
<section class="px-4 mt-4">
<section class="mt-4 px-4">
<div class="flex h-10 w-full items-center justify-between text-sm">
<Text color="muted">{$t('tags')}</Text>
</div>
<section class="flex flex-wrap pt-2 gap-1" data-testid="detail-panel-tags">
<section class="flex flex-wrap gap-1 pt-2" data-testid="detail-panel-tags">
{#each tags as tag (tag.id)}
<Badge
onClose={() => handleRemove(tag.id)}

View File

@ -20,7 +20,7 @@
};
</script>
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
<div transition:fade={{ duration: 150 }} class="flex h-full place-content-center place-items-center select-none">
{#await Promise.all([loadAssetData(assetId), import('./PhotoSphereViewerAdapter.svelte')])}
<LoadingSpinner />
{:then [data, { default: PhotoSphereViewer }]}

View File

@ -12,7 +12,7 @@
<button
type="button"
class="my-auto mx-4 rounded-full p-3 text-gray-500 transition hover:bg-gray-500 hover:text-white"
class="mx-4 my-auto rounded-full p-3 text-gray-500 transition hover:bg-gray-500 hover:text-white"
aria-label={label}
onclick={onClick}
>

View File

@ -44,12 +44,12 @@
<div
class={[
'absolute left-0 top-0 flex items-center justify-center',
'border-2 border-blue-500 pointer-events-auto cursor-text',
'focus:z-1 focus:border-blue-600 focus:border-3 focus:outline-none',
'absolute top-0 left-0 flex items-center justify-center',
'pointer-events-auto cursor-text border-2 border-blue-500',
'focus:z-1 focus:border-3 focus:border-blue-600 focus:outline-none',
isTouch
? 'text-white bg-black/60 select-all'
: 'select-text text-transparent bg-blue-500/10 transition-colors hover:z-1 hover:text-white hover:bg-black/60 hover:border-blue-600 hover:border-3',
? 'bg-black/60 text-white select-all'
: 'bg-blue-500/10 text-transparent transition-colors select-text hover:z-1 hover:border-3 hover:border-blue-600 hover:bg-black/60 hover:text-white',
ocrBox.verticalMode === 'none' ? 'px-2 py-1 whitespace-nowrap' : 'px-1 py-2',
]}
style="font-size: {fontSize}; width: {dimensions.width}px; height: {dimensions.height}px; transform: {transform}; transform-origin: 0 0; touch-action: none; {verticalStyle}"

View File

@ -8,7 +8,7 @@
<IconButton
title={ocrManager.showOverlay ? $t('hide_text_recognition') : $t('show_text_recognition')}
icon={mdiTextRecognition}
class={"dark {ocrStore.showOverlay ? 'bg-immich-primary text-white dark' : 'dark'}"}
class={"dark {ocrStore.showOverlay ? 'bg-immich-primary dark' : 'dark'} text-white"}
color="secondary"
variant="ghost"
shape="round"

View File

@ -250,7 +250,7 @@
<AssetViewerEvents {onZoom} />
<svelte:document use:shortcuts={[{ shortcut: { key: 'z' }, onShortcut: onZoom, preventDefault: true }]} />
<div class="h-full w-full mb-0" bind:this={container}></div>
<div class="mb-0 size-full" bind:this={container}></div>
<style>
/* Reset the default tooltip styling */

View File

@ -210,7 +210,7 @@
<div
bind:this={element}
class="relative h-full w-full select-none"
class="relative size-full select-none"
bind:clientWidth={containerWidth}
bind:clientHeight={containerHeight}
role="presentation"
@ -237,15 +237,15 @@
>
{#snippet backdrop()}
{#if blurredSlideshow}
<Thumbhash base64ThumbHash={asset.thumbhash!} class="absolute top-0 left-0 inset-s-0 h-dvh w-dvw" />
<Thumbhash base64ThumbHash={asset.thumbhash!} class="absolute inset-s-0 top-0 left-0 h-dvh w-dvw" />
{/if}
{/snippet}
{#snippet overlays()}
<div
class="absolute inset-0 pointer-events-none transition-opacity duration-150"
class="pointer-events-none absolute inset-0 transition-opacity duration-150"
style:opacity={isHighlighting ? 1 : 0}
>
<svg class="absolute inset-0 w-full h-full">
<svg class="absolute inset-0 size-full">
<defs>
<mask id="face-dim-mask">
<rect width="100%" height="100%" fill="white" />
@ -261,7 +261,7 @@
{@const isActive = assetViewerManager.highlightedFaces.some((f) => f.id === boundingbox.id)}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute pointer-events-auto rounded-lg {isActive && 'border-solid border-white border-3'}"
class="pointer-events-auto absolute rounded-lg {isActive && 'border-3 border-solid border-white'}"
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
onpointerenter={() => assetViewerManager.setHighlightedFaces([boundingbox.face])}
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
@ -269,7 +269,7 @@
{#if isActive && boundingbox.name}
<div
aria-hidden="true"
class="absolute bg-white/90 text-black px-2 py-1 rounded text-sm font-medium whitespace-nowrap shadow-lg"
class="absolute rounded-sm bg-white/90 px-2 py-1 text-sm font-medium whitespace-nowrap text-black shadow-lg"
style="top: {boundingbox.height + 4}px; right: 0;"
>
{boundingbox.name}

View File

@ -172,7 +172,7 @@
{#if showControls}
<div
class="m-4 flex gap-2 dark"
class="dark m-4 flex gap-2"
onmouseenter={() => (isOverControls = true)}
onmouseleave={() => (isOverControls = false)}
transition:fly={{ duration: 150 }}

View File

@ -124,12 +124,12 @@
{#if showVideo}
<div
transition:fade={{ duration: assetViewerFadeDuration }}
class="flex h-full select-none place-content-center place-items-center"
class="flex h-full place-content-center place-items-center select-none"
bind:clientWidth={containerWidth}
bind:clientHeight={containerHeight}
>
{#if castManager.isCasting}
<div class="place-content-center h-full place-items-center">
<div class="h-full place-content-center place-items-center">
<VideoRemoteViewer
poster={getAssetMediaUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
{onVideoStarted}

View File

@ -19,7 +19,7 @@
]);
</script>
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
<div transition:fade={{ duration: 150 }} class="flex h-full place-content-center place-items-center select-none">
{#await modules}
<LoadingSpinner />
{:then [PhotoSphereViewer, adapter, videoPlugin]}

View File

@ -68,12 +68,12 @@
}
</script>
<span class="flex items-center space-x-2 text-gray-200 text-2xl font-bold">
<span class="flex items-center space-x-2 text-2xl font-bold text-gray-200">
<Icon icon={mdiCastConnected} class="text-primary" size="36" />
<span>{$t('connected_to')} {castManager.receiverName}</span>
</span>
<img src={poster} alt="poster" class="rounded-xl m-4" />
<img src={poster} alt="poster" class="m-4 rounded-xl" />
<div class="flex place-content-center place-items-center">
{#if castManager.castState == CastState.BUFFERING}
@ -97,6 +97,6 @@
max={castManager.duration}
value={castManager.currentTime ?? 0}
onchange={handleSeek}
class="w-full h-4 bg-primary"
class="h-4 w-full bg-primary"
/>
</div>

View File

@ -54,8 +54,8 @@
]}
/>
<section class="relative flex flex-col h-full p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg dark pt-3">
<HStack class="justify-between me-4">
<section class="dark relative flex h-full flex-col p-2 pt-3 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<HStack class="me-4 justify-between">
<HStack>
<IconButton
shape="round"
@ -65,7 +65,7 @@
aria-label={$t('close')}
onclick={closeEditor}
/>
<p class="text-lg text-immich-fg dark:text-immich-dark-fg capitalize">{$t('editor')}</p>
<p class="text-lg text-immich-fg capitalize dark:text-immich-dark-fg">{$t('editor')}</p>
</HStack>
<Button shape="round" size="small" onclick={applyEdits} loading={editManager.isApplyingEdits}>{$t('save')}</Button>
</HStack>

View File

@ -60,9 +60,9 @@
});
</script>
<div class="flex flex-col items-center justify-center w-full h-full p-8" bind:this={canvasContainer}>
<div class="flex size-full flex-col items-center justify-center p-8" bind:this={canvasContainer}>
<div
class="crop-area max-w-full max-h-full transition-transform motion-reduce:transition-none"
class="crop-area max-h-full max-w-full transition-transform motion-reduce:transition-none"
class:rotated={transformManager.normalizedRotation % 180 > 0}
style:rotate={transformManager.imageRotation + 'deg'}
bind:this={transformManager.cropAreaEl}
@ -72,12 +72,12 @@
draggable="false"
src={imageSrc}
alt={$getAltText(toTimelineAsset(asset))}
class="h-full select-none transition-transform motion-reduce:transition-none"
class="h-full transition-transform select-none motion-reduce:transition-none"
style:transform={imageTransform}
/>
<div
class={[
'overlay w-full h-full absolute top-0 transition-colors motion-reduce:transition-none pointer-events-none',
'overlay pointer-events-none absolute top-0 size-full transition-colors motion-reduce:transition-none',
transformManager.isInteracting ? 'bg-black/30' : 'bg-black/56',
]}
bind:this={transformManager.overlayEl}
@ -86,7 +86,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={[
'grid w-full h-full cursor-move transition-opacity motion-reduce:transition-none',
'grid size-full cursor-move transition-opacity motion-reduce:transition-none',
transformManager.isInteracting ? 'opacity-100' : 'opacity-0',
]}
onmousedown={(e) => transformManager.handleMouseDownOn(e, ResizeBoundary.None)}

View File

@ -77,7 +77,7 @@
/>
<div class="mt-3 px-4">
<div class="flex h-10 w-full items-center justify-between text-sm mt-2">
<div class="mt-2 flex h-10 w-full items-center justify-between text-sm">
<h2>{$t('editor_orientation')}</h2>
</div>
<HStack>
@ -111,16 +111,16 @@
/>
</HStack>
<div class="flex h-10 w-full items-center justify-between text-sm mt-6">
<div class="mt-6 flex h-10 w-full items-center justify-between text-sm">
<h2>{$t('crop')}</h2>
</div>
<!-- Aspect Ratio Grid -->
<div class="grid grid-cols-2 mb-4">
<div class="mb-4 grid grid-cols-2">
{#each aspectRatios as ratio (ratio.value)}
<HStack>
<Button
class="w-14 h-14 m-2"
class="m-2 size-14"
shape="round"
onclick={() => selectAspectRatio(ratio)}
aria-label={ratio.label}
@ -130,14 +130,14 @@
{#if ratio.isFree}
<!-- Free crop icon with dashed border -->
<div
class="w-6 h-6 border-2 border-dashed rounded-xs flex-shrink-0 {ratioSelected(ratio)
class="size-6 shrink-0 rounded-xs border-2 border-dashed {ratioSelected(ratio)
? 'border-black'
: 'border-white'}"
></div>
{:else}
<!-- Aspect ratio box -->
<div
class="border-2 rounded-xs flex-shrink-0 {ratioSelected(ratio) ? 'border-black' : 'border-white'}"
class="shrink-0 rounded-xs border-2 {ratioSelected(ratio) ? 'border-black' : 'border-white'}"
style="width: {ratio.width}px; height: {ratio.height}px;"
></div>
{/if}

View File

@ -364,34 +364,34 @@
<div
id="face-editor-data"
class="absolute inset-s-0 top-0 z-5 h-full w-full overflow-hidden"
class="absolute inset-s-0 top-0 z-5 size-full overflow-hidden"
data-overlay-interactive
data-face-left={faceBoxPosition.left}
data-face-top={faceBoxPosition.top}
data-face-width={faceBoxPosition.width}
data-face-height={faceBoxPosition.height}
>
<canvas bind:this={canvasEl} id="face-editor" class="absolute top-0 inset-s-0"></canvas>
<canvas bind:this={canvasEl} id="face-editor" class="absolute inset-s-0 top-0"></canvas>
<div
id="face-selector"
bind:this={faceSelectorEl}
class="absolute top-[calc(50%-250px)] inset-s-[calc(50%-125px)] max-w-62.5 w-62.5 bg-white dark:bg-immich-dark-gray dark:text-immich-dark-fg backdrop-blur-sm px-2 py-4 rounded-xl border border-gray-200 dark:border-gray-800 transition-[top,left] duration-200 ease-out"
class="absolute inset-s-[calc(50%-125px)] top-[calc(50%-250px)] w-62.5 max-w-62.5 rounded-xl border border-gray-200 bg-white px-2 py-4 backdrop-blur-sm transition-[top,left] duration-200 ease-out dark:border-gray-800 dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<p class="text-center text-sm">{$t('select_person_to_tag')}</p>
<div class="my-3 relative">
<div class="relative my-3">
<Input placeholder={$t('search_people')} bind:value={searchTerm} bind:ref={searchInputEl} size="tiny" />
</div>
<div bind:this={scrollableListEl} class="h-62.5 overflow-y-auto mt-2">
<div bind:this={scrollableListEl} class="mt-2 h-62.5 overflow-y-auto">
{#if filteredCandidates.length > 0}
<div class="mt-2 rounded-lg">
{#each filteredCandidates as person (person.id)}
<button
onclick={() => tagFace(person)}
type="button"
class="w-full flex place-items-center gap-2 rounded-lg ps-1 pe-4 py-2 hover:bg-immich-primary/25"
class="flex w-full place-items-center gap-2 rounded-lg py-2 ps-1 pe-4 hover:bg-immich-primary/25"
>
<ImageThumbnail
curve

View File

@ -25,7 +25,7 @@
style:height
>
<div class="hidden @min-[75px]:block">
<Icon icon={mdiImageBrokenVariant} size="7em" class="max-w-full min-w-6 min-h-6" />
<Icon icon={mdiImageBrokenVariant} size="7em" class="min-h-6 max-w-full min-w-6" />
</div>
{#if !hideMessage}
<span class="text-center text-xs @min-[100px]:text-sm @min-[150px]:text-base">{$t('error_loading_image')}</span>

View File

@ -78,7 +78,7 @@
src={url}
onLoad={setLoaded}
onError={setErrored}
class={['object-cover bg-gray-300 dark:bg-gray-700', sharedClasses, imageClass]}
class={['bg-gray-300 object-cover dark:bg-gray-700', sharedClasses, imageClass]}
{style}
alt={loaded || errored ? altText : ''}
draggable={false}
@ -88,7 +88,7 @@
{/if}
{#if hidden}
<div class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<div class="absolute inset-s-1/2 top-1/2 translate-[-50%] transform">
<!-- TODO fix `title` type -->
<Icon title={title ?? undefined} icon={mdiEyeOffOutline} size="2em" class={hiddenIconClass} />
</div>

View File

@ -206,7 +206,7 @@
<div
class={[
'group focus-visible:outline-none flex overflow-hidden transition-[background-color,border-radius]',
'group flex overflow-hidden transition-[background-color,border-radius] focus-visible:outline-none',
backgroundColorClass,
{ 'rounded-xl': selected },
]}
@ -237,20 +237,20 @@
role="link"
>
<div
class={['group absolute top-0 bottom-0', { 'cursor-not-allowed': disabled, 'cursor-pointer': !disabled }]}
class={['group absolute inset-y-0', { 'cursor-not-allowed': disabled, 'cursor-pointer': !disabled }]}
style:width="inherit"
style:height="inherit"
>
<div
class={[
'absolute h-full w-full select-none bg-transparent transition-transform',
'absolute size-full bg-transparent transition-transform select-none',
{ 'scale-[0.85]': selected },
{ 'rounded-xl': selected },
]}
>
<ImageThumbnail
class={[
'absolute group-focus-visible:rounded-lg transition-[border-radius]',
'absolute transition-[border-radius] group-focus-visible:rounded-lg',
{ 'rounded-xl': selected },
imageClass,
]}
@ -267,7 +267,7 @@
onComplete={(errored) => ((loaded = true), (thumbError = errored))}
/>
{#if asset.isVideo}
<div class="absolute h-full w-full pointer-events-none group-focus-visible:rounded-lg">
<div class="pointer-events-none absolute size-full group-focus-visible:rounded-lg">
<VideoThumbnail
class="group-focus-visible:rounded-lg"
url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
@ -278,7 +278,7 @@
/>
</div>
{:else if asset.isImage && asset.livePhotoVideoId}
<div class="absolute h-full w-full pointer-events-none group-focus-visible:rounded-lg">
<div class="pointer-events-none absolute size-full group-focus-visible:rounded-lg">
<VideoThumbnail
class="group-focus-visible:rounded-lg"
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
@ -292,7 +292,7 @@
</div>
{:else if asset.isImage && asset.duration && mouseOver}
<!-- GIF -->
<div class="absolute h-full w-full pointer-events-none">
<div class="pointer-events-none absolute size-full">
<ImageThumbnail
class={imageClass}
{brokenAssetClass}
@ -317,12 +317,12 @@
{/if}
<!-- icon overlay -->
<div class="z-2 absolute inset-0">
<div class="absolute inset-0 z-2">
<!-- Gradient overlay on hover -->
{#if !usingMobileDevice && !disabled && !asset.isVideo}
<div
class={[
'absolute h-full w-full bg-linear-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100 ',
'absolute size-full bg-linear-to-b from-black/25 via-[transparent_25%] opacity-0 transition-opacity group-hover:opacity-100',
{ 'rounded-xl group-focus-visible:rounded-lg': selected },
]}
></div>
@ -332,36 +332,33 @@
{#if dimmed && !mouseOver}
<div
id="a"
class={[
'z-2 absolute h-full w-full bg-gray-700/40 group-focus-visible:rounded-lg',
{ 'rounded-xl': selected },
]}
class={['absolute z-2 size-full bg-gray-700/40 group-focus-visible:rounded-lg', { 'rounded-xl': selected }]}
></div>
{/if}
<!-- Favorite asset star -->
{#if !authManager.isSharedLink && asset.isFavorite}
<div class="z-2 absolute bottom-2 inset-s-2">
<div class="absolute inset-s-2 bottom-2 z-2">
<Icon data-icon-favorite icon={mdiHeart} size="24" class="text-white" />
</div>
{/if}
{#if !!assetOwner}
<div class="z-2 absolute bottom-1 inset-e-2 max-w-[50%]">
<p class="text-xs font-medium text-white drop-shadow-lg max-w-full truncate">
<div class="absolute inset-e-2 bottom-1 z-2 max-w-[50%]">
<p class="max-w-full truncate text-xs font-medium text-white drop-shadow-lg">
{assetOwner.name}
</p>
</div>
{/if}
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
<div class={['z-2 absolute inset-s-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
<div class={['absolute inset-s-2 z-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
<Icon data-icon-archive icon={mdiArchiveArrowDownOutline} size="24" class="text-white" />
</div>
{/if}
{#if asset.isImage && asset.projectionType === ProjectionType.EQUIRECTANGULAR}
<div class="z-2 absolute inset-e-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
<div class="absolute inset-e-0 top-0 z-2 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pe-2 pt-2">
<Icon icon={mdiRotate360} size="24" />
</span>
@ -369,7 +366,7 @@
{/if}
{#if asset.isImage && asset.duration}
<div class="z-2 absolute inset-e-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
<div class="absolute inset-e-0 top-0 z-2 flex place-items-center gap-1 text-xs font-medium text-white">
<span class="pe-2 pt-2">
<Icon icon={mouseOver ? mdiMotionPauseOutline : mdiFileGifBox} size="24" />
</span>
@ -380,11 +377,11 @@
{#if asset.stack && showStackedIcon}
<div
class={[
'z-2 absolute flex place-items-center gap-1 text-xs font-medium text-white',
asset.isImage && !asset.livePhotoVideoId ? 'top-0 inset-e-0' : 'top-7 inset-e-1',
'absolute z-2 flex place-items-center gap-1 text-xs font-medium text-white',
asset.isImage && !asset.livePhotoVideoId ? 'inset-e-0 top-0' : 'inset-e-1 top-7',
]}
>
<span class="pe-2 pt-2 flex place-items-center gap-1">
<span class="flex place-items-center gap-1 pe-2 pt-2">
<p>{asset.stack.assetCount.toLocaleString($locale)}</p>
<Icon icon={mdiCameraBurst} size="24" />
</span>
@ -395,7 +392,7 @@
<!-- lazy show the url on mouse over-->
{#if !usingMobileDevice && mouseOver && !disableLinkMouseOver}
<a
class="z-2 absolute w-full top-0 bottom-0"
class="absolute inset-y-0 z-2 w-full"
style:cursor="unset"
href={currentUrlReplaceAssetId(asset.id)}
onclick={(evt) => evt.preventDefault()}
@ -408,7 +405,7 @@
{#if selectionCandidate}
<div
class={['z-2 absolute top-0 h-full w-full bg-immich-primary opacity-40', { 'rounded-xl': selected }]}
class={['absolute top-0 z-2 size-full bg-immich-primary opacity-40', { 'rounded-xl': selected }]}
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
></div>
@ -446,7 +443,7 @@
e.preventDefault();
onPreview?.($state.snapshot(asset));
}}
class="absolute z-2 bottom-1 end-1 rounded-full bg-black/25 p-1.5 hover:bg-black/50 focus:outline-none transition-colors"
class="absolute inset-e-1 bottom-1 z-2 rounded-full bg-black/25 p-1.5 transition-colors hover:bg-black/50 focus:outline-none"
in:fade={{ duration: 100 }}
tabindex={-1}
aria-label="Preview asset"
@ -458,7 +455,7 @@
<!-- Outline on focus -->
<div
class={[
'pointer-events-none absolute z-1 size-full outline-immich-primary dark:outline-immich-dark-primary group-focus-visible:outline-4 group-focus-visible:-outline-offset-4',
'pointer-events-none absolute z-1 size-full outline-immich-primary group-focus-visible:outline-4 group-focus-visible:-outline-offset-4 dark:outline-immich-dark-primary',
{ 'rounded-xl': selected },
]}
data-outline

View File

@ -94,10 +94,10 @@
{/if}
<div
class="@container absolute inset-x-0 top-0 flex justify-end place-items-center gap-1 text-xs font-medium text-white text-shadow-[1px_1px_6px_rgb(0_0_0)]"
class="@container absolute inset-x-0 top-0 flex place-items-center justify-end gap-1 text-xs font-medium text-white text-shadow-[1px_1px_6px_rgb(0_0_0)]"
>
{#if showTime}
<span class="hidden @min-[100px]:inline pt-2">
<span class="hidden pt-2 @min-[100px]:inline">
{#if remainingSeconds < 60}
{Duration.fromObject({ seconds: remainingSeconds }).toFormat('m:ss')}
{:else if remainingSeconds < 3600}
@ -110,7 +110,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
class="pe-2 pt-2 @max-[99px]:scale-75 @max-[99px]:pe-1 @max-[99px]:pt-1 drop-shadow-[1px_1px_6px_rgb(0_0_0)]"
class="pe-2 pt-2 drop-shadow-[1px_1px_6px_rgb(0_0_0)] @max-[99px]:scale-75 @max-[99px]:pe-1 @max-[99px]:pt-1"
onmouseenter={onMouseEnter}
onmouseleave={onMouseLeave}
>

View File

@ -73,7 +73,7 @@
<section
transition:fly={{ x: 360, duration: 100, easing: linear }}
class="absolute top-0 h-full w-90 overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
class="absolute top-0 h-full w-90 overflow-x-hidden bg-light p-2 dark:text-immich-dark-fg"
>
<div class="flex place-items-center justify-between gap-2">
{#if !searchFaces}
@ -123,7 +123,7 @@
aria-label={$t('back')}
onclick={onClose}
/>
<div class="w-full flex">
<div class="flex w-full">
<SearchPeople
type="input"
bind:searchName
@ -146,14 +146,14 @@
/>
{/if}
</div>
<div class="px-4 py-4 text-sm">
<h2 class="mb-8 mt-4">{$t('all_people')}</h2>
<div class="p-4 text-sm">
<h2 class="mt-4 mb-8">{$t('all_people')}</h2>
{#if isShowLoadingPeople}
<div class="flex w-full justify-center">
<LoadingSpinner />
</div>
{:else}
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
<div class="mt-4 flex flex-wrap gap-2 overflow-y-auto immich-scrollbar">
{#each showPeople as person (person.id)}
{#if !editedFace.person || person.id !== editedFace.person.id}
<div class="w-fit">

View File

@ -202,7 +202,7 @@
<section
transition:fly={{ x: 360, duration: 100, easing: linear }}
class="absolute top-0 h-full w-90 overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
class="absolute top-0 h-full w-90 overflow-x-hidden bg-light p-2 dark:text-immich-dark-fg"
>
<div class="flex place-items-center justify-between gap-2">
<div class="flex items-center gap-2">
@ -229,7 +229,7 @@
{/if}
</div>
<div class="px-4 py-4 text-sm">
<div class="p-4 text-sm">
<div class="mt-4 flex flex-wrap gap-2">
{#if isShowLoadingPeople}
<div class="flex w-full justify-center">
@ -243,7 +243,7 @@
<div
role="button"
tabindex={index}
class="absolute start-0 top-0 h-22.5 w-22.5 cursor-default"
class="absolute inset-s-0 top-0 size-22.5 cursor-default"
onfocus={() => assetViewerManager.setHighlightedFaces([peopleWithFaces[index]])}
onpointerenter={() => assetViewerManager.setHighlightedFaces([peopleWithFaces[index]])}
onpointerleave={() => assetViewerManager.clearHighlightedFaces()}
@ -324,7 +324,7 @@
</p>
{/if}
<div class="absolute -end-[3px] -top-[3px] h-5 w-5 rounded-full">
<div class="absolute inset-e-[-3px] top-[-3px] size-5 rounded-full">
{#if selectedPersonToCreate[face.id] || selectedPersonToReassign[face.id]}
<IconButton
shape="round"
@ -333,7 +333,7 @@
icon={mdiRestart}
aria-label={$t('reset')}
size="small"
class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
class="absolute inset-s-1/2 top-1/2 translate-[-50%] transform"
onclick={() => handleReset(face.id)}
/>
{:else}
@ -343,29 +343,29 @@
icon={mdiPencil}
aria-label={$t('select_new_face')}
size="small"
class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
class="absolute inset-s-1/2 top-1/2 translate-[-50%] transform"
onclick={() => handleFacePicker(face)}
/>
{/if}
</div>
<div class="absolute end-8 -top-[3px] h-5 w-5 rounded-full">
<div class="absolute inset-e-8 top-[-3px] size-5 rounded-full">
{#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person}
<div
class="flex place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
class="absolute inset-s-1/2 top-1/2 flex translate-[-50%] transform place-content-center place-items-center rounded-full bg-[#d3d3d3] p-1 transition-all"
>
<Icon color="primary" icon={mdiAccountOff} aria-hidden size="24" />
</div>
{/if}
</div>
{#if face.person != null}
<div class="absolute -end-[3px] top-8 h-5 w-5 rounded-full">
<div class="absolute inset-e-[-3px] top-8 size-5 rounded-full">
<IconButton
shape="round"
color="danger"
icon={mdiTrashCan}
aria-label={$t('delete_face')}
size="small"
class="absolute start-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
class="absolute inset-s-1/2 top-1/2 translate-[-50%] transform"
onclick={() => deleteAssetFace(face)}
/>
</div>

View File

@ -25,9 +25,9 @@
</AppShellHeader>
<AppShellSidebar
bind:open={sidebarStore.isOpen}
class="border-none shadow-none h-full flex flex-col justify-between gap-2"
class="flex h-full flex-col justify-between gap-2 border-none shadow-none"
>
<div class="flex flex-col pt-8 pe-4 gap-1">
<div class="flex flex-col gap-1 pe-4 pt-8">
<NavbarItem title={$t('users')} href={Route.users()} icon={mdiAccountMultipleOutline} />
<NavbarItem title={$t('external_libraries')} href={Route.libraries()} icon={mdiBookshelf} />
<NavbarItem title={$t('admin.queues')} href={Route.queues()} icon={mdiTrayFull} />
@ -36,7 +36,7 @@
<NavbarItem title={$t('server_stats')} href={Route.systemStatistics()} icon={mdiServer} />
</div>
<div class="mb-2 me-4">
<div class="me-4 mb-2">
<BottomInfo />
</div>
</AppShellSidebar>

View File

@ -11,21 +11,21 @@
let { title, children, withHeader = true, withBackdrop = true }: Props = $props();
</script>
<section class="min-w-dvw flex min-h-dvh items-center justify-center relative isolate">
<section class="relative isolate flex min-h-dvh min-w-dvw items-center justify-center">
{#if withBackdrop}
<div class="absolute -z-10 w-full h-full flex place-items-center place-content-center">
<div class="absolute -z-10 flex size-full place-content-center place-items-center">
<img
src={immichLogo}
class="max-w-(--breakpoint-md) mx-auto h-full mb-2 antialiased overflow-hidden"
class="mx-auto mb-2 h-full max-w-(--breakpoint-md) overflow-hidden antialiased"
alt="Immich logo"
/>
<div
class="w-full h-[99%] absolute inset-s-0 top-0 backdrop-blur-[200px] bg-transparent dark:bg-immich-dark-bg/20"
class="absolute inset-s-0 top-0 h-[99%] w-full bg-transparent backdrop-blur-[200px] dark:bg-immich-dark-bg/20"
></div>
</div>
{/if}
<Card color="secondary" class="w-full max-w-xl border m-2">
<Card color="secondary" class="m-2 w-full max-w-xl border">
{#if withHeader}
<CardHeader class="mt-6">
<VStack>

View File

@ -71,9 +71,9 @@
{#if title || buttons}
<div class="absolute flex h-16 w-full place-items-center justify-between border-b p-2 text-dark">
<div class="flex gap-2 items-center">
<div class="flex items-center gap-2">
{#if title}
<div class="outline-none pe-8" tabindex="-1" id={headerId}>{title}</div>
<div class="pe-8 outline-none" tabindex="-1" id={headerId}>{title}</div>
{/if}
{#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>

View File

@ -57,9 +57,9 @@
<OnEvents {onBackupDeleteStatus} />
<Card class="dark:bg-dark-900">
<CardBody class="pt-3 pb-4 px-6">
<Stack gap={3} class="grow min-w-0">
<div class="flex justify-between items-center gap-3">
<CardBody class="px-6 pt-3 pb-4">
<Stack gap={3} class="min-w-0 grow">
<div class="flex items-center justify-between gap-3">
<HStack gap={2} class="min-w-0">
{#if status === BackupFileStatus.OK}
<Icon icon={mdiCheckCircle} size="18" class="text-success" />
@ -76,7 +76,7 @@
{/if}
{#if relativeTime}
<div class="flex items-center gap-2">
<div class="w-1 h-1 bg-light-500"></div>
<div class="size-1 bg-light-500"></div>
<Text size="tiny" color="muted">{relativeTime}</Text>
</div>
{/if}
@ -97,7 +97,7 @@
<HStack>
<Icon icon={mdiDatabaseRefreshOutline} size="16" color="gray" />
<Text size="small" class="break-all font-mono">{filename}</Text>
<Text size="small" class="font-mono break-all">{filename}</Text>
</HStack>
{#if status === BackupFileStatus.UnknownVersion}

View File

@ -91,8 +91,8 @@
<Card color="info">
<CardBody>
{#if uploadProgress === -1}
<div class="flex justify-between items-center">
<div class="flex gap-2 items-end w-max">
<div class="flex items-center justify-between">
<div class="flex w-max items-end gap-2">
<Icon icon={mdiTrayArrowUp} size="20" class="text-muted"></Icon>
<Text class="grow">{$t('admin.maintenance_upload_backup')}</Text>
</div>
@ -118,7 +118,7 @@
{#each [...groupedBackups.entries()] as [dateGroup, groupBackups] (dateGroup)}
<Stack gap={2}>
<div class="mt-5 mb-1">
<div class="bg-primary-50 flex gap-2 px-4 py-2 rounded-xl w-max place-items-center">
<div class="flex w-max place-items-center gap-2 rounded-xl bg-primary-50 px-4 py-2">
<Icon icon={mdiCalendar} size="18" />
<Text size="small" fontWeight="medium" color="muted">{dateGroup}</Text>
</div>

View File

@ -7,7 +7,7 @@
<title>{$t('error')} - Immich</title>
</svelte:head>
<section class="flex flex-col px-4 h-dvh w-dvw place-content-center place-items-center">
<section class="flex h-dvh w-dvw flex-col place-content-center place-items-center px-4">
<h1 class="py-10 text-4xl text-primary"><span>{$t('errors.page_not_found')}</span><span class="ps-3">:/</span></h1>
{#if page.error?.message}
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>

View File

@ -71,9 +71,9 @@
</svelte:head>
{#if passwordRequired}
<main
class="relative h-dvh overflow-hidden px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height) sm:px-12 md:px-24 lg:px-40"
class="relative h-dvh overflow-hidden px-6 pt-(--navbar-height) max-md:pt-(--navbar-height-md) sm:px-12 md:px-24 lg:px-40"
>
<div class="flex flex-col items-center justify-center mt-20">
<div class="mt-20 flex flex-col items-center justify-center">
<div class="text-2xl font-bold text-primary">{$t('password_required')}</div>
<div class="mt-4 text-lg text-primary">
{$t('sharing_enter_password')}

View File

@ -25,25 +25,25 @@
};
</script>
<div class="flex h-35 w-full flex-col justify-between rounded-3xl bg-subtle text-primary p-5">
<div class="flex h-35 w-full flex-col justify-between rounded-3xl bg-subtle p-5 text-primary">
<div class="flex place-items-center gap-4">
<Icon {icon} size="40" />
<Text size="giant" fontWeight="medium">{title}</Text>
</div>
{#await valuePromise}
<div class="mx-auto font-mono text-2xl font-medium relative">
<span class="text-gray-300 dark:text-gray-600 shimmer-text">{zeros()}</span>
<div class="relative mx-auto font-mono text-2xl font-medium">
<span class="shimmer-text text-gray-300 dark:text-gray-600">{zeros()}</span>
</div>
{:then data}
<div class="mx-auto font-mono text-2xl font-medium relative">
<div class="relative mx-auto font-mono text-2xl font-medium">
<span class="text-gray-300 dark:text-gray-600">{zeros(data)}</span><span>{data.value}</span>
{#if data.unit}
<code class="font-mono text-base font-normal">{data.unit}</code>
{/if}
</div>
{:catch _}
<div class="mx-auto font-mono text-2xl font-medium relative">
<div class="relative mx-auto font-mono text-2xl font-medium">
<span class="text-gray-300 dark:text-gray-600">{zeros()}</span>
</div>
{/await}

View File

@ -74,11 +74,11 @@
</script>
{#if sharedLink?.allowUpload || assets.length > 1}
<main class="mt-24 mb-40 mx-4 isolate" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
<main class="isolate mx-4 mt-24 mb-40" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
<GalleryViewer {assets} assetInteraction={assetMultiSelectManager} {viewport} allowDeletion={false} />
</main>
<header class="fixed top-0 inset-s-0 w-full">
<header class="fixed inset-s-0 top-0 w-full">
{#if assetMultiSelectManager.selectionActive}
<AssetSelectControlBar>
<IconButton

View File

@ -268,9 +268,9 @@
</script>
<svelte:window onresize={onPositionChange} />
<Label class="block mb-1 {hideLabel ? 'sr-only' : ''} text-xs text-neutral-500 font-light" for={inputId}>{label}</Label>
<Label class="mb-1 block {hideLabel ? 'sr-only' : ''} text-xs font-light text-neutral-500" for={inputId}>{label}</Label>
<div
class="relative w-full dark:text-gray-300 text-gray-700 text-base"
class="relative w-full text-base text-gray-700 dark:text-gray-300"
use:focusOutside={{ onFocusOut: deactivate }}
use:shortcuts={[
{
@ -284,7 +284,7 @@
>
<div>
{#if isActive}
<div class="absolute inset-y-0 start-0 flex items-center ps-3">
<div class="absolute inset-y-0 inset-s-0 flex items-center ps-3">
<div class="dark:text-immich-dark-fg/75">
<Icon icon={mdiMagnify} aria-hidden />
</div>
@ -300,11 +300,11 @@
aria-expanded={isOpen}
autocomplete="off"
bind:this={input}
class:!ps-8={isActive}
class:!rounded-b-none={isOpen && dropdownDirection === 'bottom'}
class:!rounded-t-none={isOpen && dropdownDirection === 'top'}
class:ps-8!={isActive}
class:rounded-b-none!={isOpen && dropdownDirection === 'bottom'}
class:rounded-t-none!={isOpen && dropdownDirection === 'top'}
class:cursor-pointer={!isActive}
class="immich-form-input text-sm w-full pe-12! transition-all"
class="immich-form-input w-full pe-12! text-sm transition-all"
id={inputId}
onfocus={activate}
oninput={onInput}
@ -355,7 +355,7 @@
/>
<div
class="absolute end-0 top-0 h-full flex px-4 justify-center items-center content-between"
class="absolute inset-e-0 top-0 flex h-full content-between items-center justify-center px-4"
class:pe-2={selectedOption}
class:pointer-events-none={!selectedOption}
>
@ -379,10 +379,10 @@
role="listbox"
id={listboxId}
in:fly={{ duration: 250 }}
class="fixed z-1 text-start text-sm w-full overflow-y-auto bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-900"
class="fixed z-1 w-full overflow-y-auto border-gray-300 bg-white text-start text-sm dark:border-gray-900 dark:bg-gray-800"
class:rounded-b-xl={dropdownDirection === 'bottom'}
class:rounded-t-xl={dropdownDirection === 'top'}
class:shadow={dropdownDirection === 'bottom'}
class:shadow-sm={dropdownDirection === 'bottom'}
class:border={isOpen}
style:top={position?.top}
style:bottom={position?.bottom}
@ -398,7 +398,7 @@
role="option"
aria-selected={selectedIndex === 0}
aria-disabled={true}
class="text-start w-full px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-200 aria-selected:dark:bg-gray-700"
class="w-full cursor-default px-4 py-2 text-start hover:bg-gray-200 aria-selected:bg-gray-200 dark:hover:bg-gray-700 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${0}`}
onclick={closeDropdown}
>
@ -410,7 +410,7 @@
<li
aria-selected={index === selectedIndex}
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 wrap-break-words"
class="wrap-break-words w-full cursor-pointer px-4 py-2 text-start transition-all hover:bg-gray-200 aria-selected:bg-gray-200 dark:hover:bg-gray-700 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${index}`}
onclick={() => handleSelect(option)}
role="option"

View File

@ -66,12 +66,12 @@
!multiRow && 'grid-cols-[10%_80%_10%] sm:grid-cols-[25%_50%_25%]',
'justify-between lg:grid-cols-[25%_50%_25%]',
appBarBorder,
'mx-2 my-2 place-items-center rounded-lg p-2 max-md:p-0 transition-all',
'm-2 place-items-center rounded-lg p-2 transition-all max-md:p-0',
tailwindClasses,
forceDark ? 'bg-immich-dark-gray! text-white' : 'bg-subtle dark:bg-immich-dark-gray',
]}
>
<div class="flex place-items-center sm:gap-6 justify-self-start dark:text-immich-dark-fg {forceDark ? 'dark' : ''}">
<div class="flex place-items-center justify-self-start sm:gap-6 dark:text-immich-dark-fg {forceDark ? 'dark' : ''}">
{#if showBackButton}
<IconButton
aria-label={$t('close')}
@ -90,7 +90,7 @@
{@render children?.()}
</div>
<div class="max-[350px]:me-0 max-[350px]:gap-0 me-4 flex place-items-center gap-1 justify-self-end">
<div class="me-4 flex place-items-center gap-1 justify-self-end max-[350px]:me-0 max-[350px]:gap-0">
{@render trailing?.()}
</div>
</nav>

View File

@ -28,7 +28,7 @@
<img {src} alt="" width="500" draggable="false" />
{#if title}
<h2 class="text-xl font-medium my-4">{title}</h2>
<h2 class="my-4 text-xl font-medium">{title}</h2>
{/if}
<p class="text-immich-text-gray-500 dark:text-immich-dark-fg font-light text-center">{text}</p>
<p class="text-immich-text-gray-500 text-center font-light dark:text-immich-dark-fg">{text}</p>
</svelte:element>

View File

@ -15,6 +15,6 @@
<div style="width: {width}px; height: {width}px">
{#await promise then url}
<img src={url} {alt} class="h-full w-full" />
<img src={url} {alt} class="size-full" />
{/await}
</div>

View File

@ -11,9 +11,9 @@
let { label, onRemove }: Props = $props();
</script>
<div class="flex group transition-all">
<div class="group flex transition-all">
<span
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
class="inline-block h-min rounded-s-full bg-primary py-1 ps-3 pe-1 text-center align-baseline leading-none whitespace-nowrap text-gray-100 transition-all group-hover:ps-3 hover:bg-immich-primary/80 dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80"
>
<p class="text-sm">
{label}
@ -22,7 +22,7 @@
<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-e-full place-items-center place-content-center pe-2 ps-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
class="place-content-center place-items-center rounded-e-full bg-immich-primary/95 py-1 ps-1 pe-2 text-gray-100 transition-all hover:bg-immich-primary/80 dark:bg-immich-dark-primary/95 dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80"
title={$t('remove_tag')}
onclick={onRemove}
>

View File

@ -78,7 +78,7 @@
</script>
<figure
class="{sizeClass} {colorClass} {interactiveClass} overflow-hidden shadow-md rounded-full"
class="{sizeClass} {colorClass} {interactiveClass} overflow-hidden rounded-full shadow-md"
title={noTitle ? undefined : title}
>
{#if user.profileImagePath}
@ -86,14 +86,14 @@
bind:this={img}
src={getProfileImageUrl(user)}
alt={$t('profile_image_of_user', { values: { user: title } })}
class="h-full w-full object-cover"
class="size-full object-cover"
class:hidden={showFallback}
draggable="false"
/>
{/if}
{#if showFallback}
<span
class="uppercase flex h-full w-full select-none items-center justify-center font-medium"
class="flex size-full items-center justify-center font-medium uppercase select-none"
class:text-xs={size === 'sm'}
class:text-lg={size === 'lg'}
class:text-xl={size === 'xl'}

View File

@ -26,11 +26,11 @@
type="button"
onclick={() => onNewAlbum(searchQuery)}
use:scrollIntoViewIfSelected
class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
class="flex w-full items-center gap-4 rounded-xl px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
class:bg-gray-200={selected}
class:dark:bg-gray-700={selected}
>
<div class="flex h-12 w-12 items-center justify-center">
<div class="flex size-12 items-center justify-center">
<Icon icon={mdiPlus} size="30" />
</div>
<p class="">

View File

@ -64,7 +64,7 @@
<div
bind:this={menuScrollView}
class={[
'duration-250 ease-in-out fixed min-w-50 w-max max-w-75 rounded-lg shadow-lg bg-slate-100 z-1 immich-scrollbar',
'fixed z-1 w-max max-w-75 min-w-50 rounded-lg bg-slate-100 shadow-lg duration-250 ease-in-out immich-scrollbar',
position.needScrollBar ? 'overflow-auto' : 'overflow-hidden',
]}
style:left="{position.left}px"

View File

@ -53,7 +53,7 @@
onclick={handleClick}
onmouseover={() => ($selectedIdStore = id)}
onmouseleave={() => ($selectedIdStore = undefined)}
class="w-full p-4 text-start text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive
class="w-full p-4 text-start text-sm font-medium {textColor} flex cursor-pointer items-center gap-2 border-gray-200 focus:ring-2 focus:outline-none focus:ring-inset {isActive
? activeColor
: 'bg-slate-100'}"
role="menuitem"
@ -65,7 +65,7 @@
<div class="flex justify-between">
{text}
{#if shortcutLabel}
<span class="text-gray-500 ps-4">
<span class="ps-4 text-gray-500">
{shortcutLabel}
</span>
{/if}

View File

@ -91,7 +91,7 @@
},
]}
>
<section class="fixed start-0 top-0 flex h-dvh w-dvw" {oncontextmenu} role="presentation">
<section class="fixed inset-s-0 top-0 flex h-dvh w-dvw" {oncontextmenu} role="presentation">
<ContextMenu
{direction}
{x}

View File

@ -387,7 +387,7 @@
/>
{#if showAssetName && !isTimelineAsset(asset)}
<div
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-linear-to-t bg-slate-50/75 dark:bg-slate-800/75 overflow-clip text-ellipsis whitespace-pre-wrap"
class="absolute bottom-0 w-full overflow-clip bg-slate-50/75 bg-linear-to-t p-1 text-center font-mono text-xs font-semibold text-ellipsis whitespace-pre-wrap dark:bg-slate-800/75"
>
{asset.originalFileName}
</div>

View File

@ -390,7 +390,7 @@
>
{#snippet children({ feature })}
<div
class="rounded-full w-10 h-10 bg-immich-primary text-white flex justify-center items-center font-mono font-bold shadow-lg hover:bg-immich-dark-primary transition-all duration-200 hover:text-immich-dark-bg opacity-90"
class="flex size-10 items-center justify-center rounded-full bg-immich-primary font-mono font-bold text-white opacity-90 shadow-lg transition-all duration-200 hover:bg-immich-dark-primary hover:text-immich-dark-bg"
>
{feature.properties?.point_count?.toLocaleString()}
</div>
@ -407,11 +407,11 @@
>
{#snippet children({ feature }: { feature: Feature })}
{#if useLocationPin}
<Icon icon={mdiMapMarker} size="50px" class="text-primary -translate-y-[50%]" />
<Icon icon={mdiMapMarker} size="50px" class="translate-y-[-50%] text-primary" />
{:else}
<img
src={getAssetMediaUrl({ id: feature.properties?.id })}
class="rounded-full w-15 h-15 border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary"
class="size-15 rounded-full border-2 border-immich-primary bg-immich-primary object-cover shadow-lg transition-all duration-200 hover:scale-150 hover:border-immich-dark-primary"
alt={feature.properties?.city && feature.properties.country
? $t('map_marker_for_images', {
values: { city: feature.properties.city, country: feature.properties.country },

View File

@ -31,7 +31,7 @@
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
id="account-info-panel"
class="absolute z-1 end-6 top-19 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray"
class="absolute inset-e-6 top-19 z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-200 shadow-lg dark:border dark:border-immich-dark-gray dark:bg-immich-dark-gray"
use:focusTrap
>
<div
@ -39,7 +39,7 @@
>
<div class="relative">
<UserAvatar user={authManager.user} size="xl" />
<div class="absolute bottom-0 end-0 rounded-full w-6 h-6">
<div class="absolute inset-e-0 bottom-0 size-6 rounded-full">
<IconButton
color="primary"
icon={mdiPencil}
@ -68,9 +68,9 @@
color="secondary"
variant="ghost"
shape="round"
class="border dark:border-immich-dark-gray dark:bg-gray-500 dark:hover:bg-immich-dark-primary/50 hover:bg-immich-primary/10 dark:text-white"
class="border hover:bg-immich-primary/10 dark:border-immich-dark-gray dark:bg-gray-500 dark:text-white dark:hover:bg-immich-dark-primary/50"
>
<div class="flex place-content-center place-items-center text-center gap-2 px-2">
<div class="flex place-content-center place-items-center gap-2 px-2 text-center">
<Icon icon={mdiCog} size="18" aria-hidden />
{$t('account_settings')}
</div>
@ -84,9 +84,9 @@
size="small"
color="secondary"
aria-current={page.url.pathname.includes('/admin') ? 'page' : undefined}
class="border dark:border-immich-dark-gray dark:bg-gray-500 dark:hover:bg-immich-dark-primary/50 hover:bg-immich-primary/10 dark:text-white"
class="border hover:bg-immich-primary/10 dark:border-immich-dark-gray dark:bg-gray-500 dark:text-white dark:hover:bg-immich-dark-primary/50"
>
<div class="flex place-content-center place-items-center text-center gap-2 px-2">
<div class="flex place-content-center place-items-center gap-2 px-2 text-center">
<Icon icon={mdiWrench} size="18" aria-hidden />
{$t('administration')}
</div>
@ -106,7 +106,7 @@
<button
type="button"
class="text-center mt-4 underline text-xs text-primary"
class="mt-4 text-center text-xs text-primary underline"
onclick={async () => {
onClose?.();
if (info) {

View File

@ -49,14 +49,14 @@
<svelte:window bind:innerWidth />
<nav id="dashboard-navbar" class="max-md:h-(--navbar-height-md) h-(--navbar-height) w-dvw text-sm">
<nav id="dashboard-navbar" class="h-(--navbar-height) w-dvw text-sm max-md:h-(--navbar-height-md)">
<SkipLink text={$t('skip_to_content')} />
<div
class="grid h-full grid-cols-[--spacing(32)_auto] items-center py-2 sidebar:grid-cols-[--spacing(64)_auto] {noBorder
? ''
: 'border-b'}"
>
<div class="flex flex-row gap-1 mx-4 items-center">
<div class="mx-4 flex flex-row items-center gap-1">
<IconButton
id={menuButtonId}
shape="round"
@ -80,14 +80,14 @@
<Logo variant={mediaQueryManager.isFullSidebar ? 'inline' : 'icon'} class="max-md:h-12" />
</a>
</div>
<div class="flex justify-between gap-4 lg:gap-8 pe-6">
<div class="hidden w-full max-w-5xl flex-1 tall:ps-0 sm:block">
<div class="flex justify-between gap-4 pe-6 lg:gap-8">
<div class="hidden w-full max-w-5xl flex-1 sm:block tall:ps-0">
{#if featureFlagsManager.value.search}
<SearchBar grayTheme={true} />
{/if}
</div>
<section class="flex place-items-center justify-end gap-1 md:gap-2 w-full sm:w-auto">
<section class="flex w-full place-items-center justify-end gap-1 sm:w-auto md:gap-2">
{#if featureFlagsManager.value.search}
<IconButton
color="secondary"
@ -146,7 +146,7 @@
{#if hasUnreadNotifications}
<div
class="pointer-events-none absolute border top-0 right-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-light"
class="pointer-events-none absolute top-0 right-1 flex size-5 items-center justify-center rounded-full border bg-primary text-[10px] font-bold text-light"
>
{notificationManager.notifications.length}
</div>

View File

@ -100,13 +100,13 @@
</script>
<button
class="min-h-20 p-2 py-3 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 border-b border-gray-200 dark:border-immich-dark-gray w-full"
class="min-h-20 w-full border-b border-gray-200 p-2 py-3 hover:bg-immich-primary/10 dark:border-immich-dark-gray dark:hover:bg-immich-dark-primary/10"
type="button"
onclick={() => onclick(notification)}
title={notification.createdAt}
>
<div class="grid grid-cols-[56px_1fr_32px] items-center gap-2">
<div class="flex place-items-center place-content-center">
<div class="flex place-content-center place-items-center">
<IconButton
icon={getIconType(notification.type)}
color={getAlertColor(notification.level)}
@ -118,7 +118,7 @@
</div>
<Stack class="text-left" gap={1}>
<Text size="tiny" class="text-black dark:text-white text-base" fontWeight="semi-bold">{notification.title}</Text>
<Text size="tiny" class="text-base text-black dark:text-white" fontWeight="semi-bold">{notification.title}</Text>
{#if notification.description}
<Text class="overflow-hidden text-gray-600 dark:text-gray-300">{notification.description}</Text>
{/if}
@ -127,7 +127,7 @@
</Stack>
{#if !notification.readAt}
<div class="w-2 h-2 rounded-full bg-primary text-right justify-self-center"></div>
<div class="size-2 justify-self-center rounded-full bg-primary text-right"></div>
{/if}
</div>
</button>

View File

@ -66,11 +66,11 @@
in:fade={{ duration: 100 }}
out:fade={{ duration: 100 }}
id="notification-panel"
class="absolute right-6 top-17.5 z-1 w-[min(360px,100vw-50px)] rounded-3xl bg-gray-100 border border-gray-200 shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray text-light"
class="absolute top-17.5 right-6 z-1 w-[min(360px,100vw-50px)] rounded-3xl border border-gray-200 bg-gray-100 text-light shadow-lg dark:border dark:border-light dark:bg-immich-dark-gray"
use:focusTrap
>
<Stack class="max-h-125">
<div class="flex justify-between items-center mt-4 mx-4">
<div class="mx-4 mt-4 flex items-center justify-between">
<Text size="medium" color="secondary" fontWeight="semi-bold">{$t('notifications')}</Text>
<div>
<Button
@ -88,7 +88,7 @@
{#if noUnreadNotifications}
<Stack
class="py-12 flex flex-col place-items-center place-content-center text-gray-700 dark:text-gray-300"
class="flex flex-col place-content-center place-items-center py-12 text-gray-700 dark:text-gray-300"
gap={1}
>
<Icon icon={mdiBellOutline} size="20"></Icon>

View File

@ -91,5 +91,5 @@
</script>
{#if !hidden}
<span class="absolute start-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`}></span>
<span class="absolute inset-s-0 h-[3px] bg-immich-primary shadow-2xl" style:width={`${$progress * 100}%`}></span>
{/if}

View File

@ -8,11 +8,11 @@
<!-- Individual Purchase Option -->
<div
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
class="w-[min(375px,100%)] rounded-3xl border border-gray-300 bg-gray-100 p-8 dark:border-gray-800 dark:bg-gray-900"
>
<div class="text-primary">
<Icon icon={mdiAccount} size="56" />
<p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>
<p class="mt-1 text-lg font-semibold">{$t('purchase_individual_title')}</p>
</div>
<div class="mt-4 dark:text-immich-gray">
@ -20,20 +20,20 @@
<p>{$t('purchase_per_user')}</p>
</div>
<div class="flex flex-col justify-between h-50 dark:text-immich-gray">
<div class="flex h-50 flex-col justify-between dark:text-immich-gray">
<div class="mt-6 flex flex-col gap-1">
<div class="grid grid-cols-[36px_auto]">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<Icon icon={mdiCheckCircleOutline} size="24" class="self-center text-green-500" />
<p class="self-center">{$t('purchase_individual_description_1')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<Icon icon={mdiCheckCircleOutline} size="24" class="self-center text-green-500" />
<p class="self-center">{$t('purchase_lifetime_description')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<Icon icon={mdiCheckCircleOutline} size="24" class="self-center text-green-500" />
<p class="self-center">{$t('purchase_individual_description_2')}</p>
</div>
</div>

View File

@ -13,12 +13,12 @@
let { onDone }: Props = $props();
</script>
<div class="m-auto w-3/4 text-center flex flex-col place-content-center place-items-center my-6">
<div class="m-auto my-6 flex w-3/4 flex-col place-content-center place-items-center text-center">
<Icon icon={mdiPartyPopper} class="text-primary" size="96" />
<p class="text-4xl mt-8 font-bold">{$t('purchase_activated_title')}</p>
<p class="text-lg mt-6">{$t('purchase_activated_subtitle')}</p>
<p class="mt-8 text-4xl font-bold">{$t('purchase_activated_title')}</p>
<p class="mt-6 text-lg">{$t('purchase_activated_subtitle')}</p>
<div class="mb-4 w-full mt-6 border rounded-xl p-4 bg-gray-50 dark:bg-gray-900 dark:border-gray-600">
<div class="mt-6 mb-4 w-full rounded-xl border bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-900">
<SettingSwitch
title={$t('show_supporter_badge')}
subtitle={$t('show_supporter_badge_description')}

View File

@ -55,7 +55,7 @@
</div>
{/if}
<div class="flex flex-col sm:flex-row gap-6 mt-4 justify-between">
<div class="mt-4 flex flex-col justify-between gap-6 sm:flex-row">
<ServerPurchaseOptionCard />
<UserPurchaseOptionCard />
</div>

View File

@ -8,11 +8,11 @@
<!-- SERVER Purchase Options -->
<div
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
class="w-[min(375px,100%)] rounded-3xl border border-gray-300 bg-gray-100 p-8 dark:border-gray-800 dark:bg-gray-900"
>
<div class="text-primary">
<Icon icon={mdiServer} size="56" />
<p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>
<p class="mt-1 text-lg font-semibold">{$t('purchase_server_title')}</p>
</div>
<div class="mt-4 dark:text-immich-gray">
@ -20,20 +20,20 @@
<p>{$t('purchase_per_server')}</p>
</div>
<div class="flex flex-col justify-between h-50 dark:text-immich-gray">
<div class="flex h-50 flex-col justify-between dark:text-immich-gray">
<div class="mt-6 flex flex-col gap-1">
<div class="grid grid-cols-[36px_auto]">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<Icon icon={mdiCheckCircleOutline} size="24" class="self-center text-green-500" />
<p class="self-center">{$t('purchase_server_description_1')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<Icon icon={mdiCheckCircleOutline} size="24" class="self-center text-green-500" />
<p class="self-center">{$t('purchase_lifetime_description')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon icon={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<Icon icon={mdiCheckCircleOutline} size="24" class="self-center text-green-500" />
<p class="self-center">{$t('purchase_server_description_2')}</p>
</div>
</div>

View File

@ -248,11 +248,11 @@
]}
/>
<div class="w-full relative z-auto" use:focusOutside={{ onFocusOut }} tabindex="-1">
<div class="relative z-auto w-full" use:focusOutside={{ onFocusOut }} tabindex="-1">
<form
draggable="false"
autocomplete="off"
class="select-text text-sm"
class="text-sm select-text"
action={Route.search()}
onreset={() => (value = '')}
{onsubmit}
@ -265,11 +265,11 @@
type="text"
name="q"
id="main-search-bar"
class="w-full transition-all border-2 ps-14 py-4 max-md:py-2 text-immich-fg/75 dark:text-immich-dark-fg
class="w-full border-2 py-4 ps-14 text-immich-fg/75 transition-all max-md:py-2 dark:text-immich-dark-fg
{showClearIcon ? 'pe-22.5' : 'pe-14'}
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
{showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
{searchStore.isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
{searchStore.isSearchEnabled ? 'border-gray-200 bg-white dark:border-gray-700' : 'border-transparent'}"
placeholder={$t('search_your_photos')}
required
pattern="^(?!m:$).*$"
@ -309,9 +309,9 @@
<div
id={searchTypeId}
class="absolute inset-y-0 flex items-center end-16"
class="absolute inset-y-0 inset-e-16 flex items-center"
class:max-md:hidden={value}
class:end-28={value.length > 0}
class:inset-e-28={value.length > 0}
>
<div class="relative" use:focusOutside={{ onFocusOut: closeSearchTypeDropdown }}>
<Button
@ -320,7 +320,7 @@
color={searchStore.isSearchEnabled ? 'primary' : 'secondary'}
class="px-3 py-1 text-xs {searchStore.isSearchEnabled
? 'border border-transparent'
: 'border border-secondary/5 text-muted hover:text-dark font-light'}"
: 'border-secondary/5 border font-light text-muted hover:text-dark'}"
onclick={toggleSearchTypeDropdown}
aria-expanded={showSearchTypeDropdown}
aria-haspopup="listbox"
@ -330,13 +330,13 @@
{#if showSearchTypeDropdown}
<div
class="absolute top-full right-0 mt-1 bg-white dark:bg-immich-dark-gray border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1 min-w-32 z-9999"
class="absolute top-full right-0 z-9999 mt-1 min-w-32 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-600 dark:bg-immich-dark-gray"
>
{#each searchTypes as searchType (searchType.value)}
<button
type="button"
tabindex="0"
class="w-full text-left px-3 py-2 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors
class="w-full px-3 py-2 text-left text-xs transition-colors hover:bg-gray-100 dark:hover:bg-gray-700
{currentSearchType === searchType.value ? 'bg-gray-100 dark:bg-gray-700' : ''}"
onclick={() => selectSearchType(searchType.value)}
>
@ -349,7 +349,7 @@
</div>
{#if showClearIcon}
<div class="absolute inset-y-0 end-0 flex items-center pe-2">
<div class="absolute inset-y-0 inset-e-0 flex items-center pe-2">
<IconButton
onclick={onClear}
icon={mdiClose}
@ -361,7 +361,7 @@
/>
</div>
{/if}
<div class="absolute inset-y-0 start-0 flex items-center ps-2">
<div class="absolute inset-y-0 inset-s-0 flex items-center ps-2">
<IconButton
type="submit"
aria-label={$t('search')}
@ -375,7 +375,7 @@
</div>
</form>
<div class="absolute inset-y-0 {showClearIcon ? 'end-14' : 'end-2'} flex items-center ps-6 transition-all">
<div class="absolute inset-y-0 {showClearIcon ? 'inset-e-14' : 'inset-e-2'} flex items-center ps-6 transition-all">
<IconButton
aria-label={$t('show_search_options')}
shape="round"

View File

@ -76,7 +76,7 @@
<div id="camera-selection">
<Text fontWeight="medium">{$t('camera')}</Text>
<div class="grid grid-auto-fit-40 gap-5 mt-1">
<div class="mt-1 grid grid-auto-fit-40 gap-5">
<div class="w-full">
<Combobox
label={$t('make')}

View File

@ -14,7 +14,7 @@
<fieldset>
<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="mt-1 flex flex-wrap gap-x-5 gap-y-2">
<div class="flex items-center gap-2">
<Checkbox id="not-in-album-checkbox" size="tiny" bind:checked={filters.isNotInAlbum} />
<Label label={$t('not_in_any_album')} for="not-in-album-checkbox" class="text-sm font-normal" />

View File

@ -94,7 +94,7 @@
{#if isOpen && isSearchSuggestions}
<div
transition:fly={{ y: 25, duration: 150 }}
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 z-1 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"
>
<div class="flex items-center justify-between px-5 pt-5 text-xs">
<Text class="py-2" color="muted" aria-hidden={true}>{$t('recent_searches')}</Text>
@ -102,7 +102,7 @@
<button
id={getId(0)}
type="button"
class="rounded-lg p-2 font-semibold text-primary aria-selected:bg-immich-primary/25 hover:bg-immich-primary/25"
class="rounded-lg p-2 font-semibold text-primary hover:bg-immich-primary/25 aria-selected:bg-immich-primary/25"
role="option"
onclick={() => handleClearAll()}
tabindex="-1"
@ -121,7 +121,7 @@
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
id={getId(index)}
class="relative flex w-full cursor-pointer gap-3 py-3 ps-5 hover:bg-gray-100 aria-selected:bg-gray-100 dark:aria-selected:bg-gray-500/30 dark:hover:bg-gray-500/30"
class="relative flex w-full cursor-pointer gap-3 py-3 ps-5 hover:bg-gray-100 aria-selected:bg-gray-100 dark:hover:bg-gray-500/30 dark:aria-selected:bg-gray-500/30"
onclick={() => handleSelect(savedSearchTerm)}
role="option"
tabindex="-1"
@ -131,7 +131,7 @@
<Icon icon={mdiMagnify} size="1.5em" aria-hidden />
{savedSearchTerm}
</div>
<div aria-hidden={true} class="absolute end-5 top-0 items-center justify-center py-3">
<div aria-hidden={true} class="absolute inset-e-5 top-0 items-center justify-center py-3">
<IconButton
shape="round"
color="secondary"

View File

@ -70,7 +70,7 @@
<div id="location-selection">
<Text fontWeight="medium">{$t('place')}</Text>
<div class="grid grid-auto-fit-40 gap-5 mt-1">
<div class="mt-1 grid grid-auto-fit-40 gap-5">
<div class="w-full">
<Combobox
label={$t('country')}

View File

@ -15,7 +15,7 @@
<fieldset>
<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="mt-1 flex flex-wrap gap-x-5 gap-y-2">
<RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label={$t('all')} value={MediaType.All} />
<RadioButton
name="media-type"

View File

@ -52,10 +52,10 @@
};
const styles = tv({
base: 'flex flex-col items-center rounded-3xl border-2 hover:bg-subtle dark:hover:bg-immich-dark-primary/20 p-2 transition-all',
base: 'flex flex-col items-center rounded-3xl border-2 p-2 transition-all hover:bg-subtle dark:hover:bg-immich-dark-primary/20',
variants: {
selected: {
true: 'dark:border-slate-500 border-slate-400 bg-slate-200 dark:bg-slate-800 dark:text-white',
true: 'border-slate-400 bg-slate-200 dark:border-slate-500 dark:bg-slate-800 dark:text-white',
false: 'border-transparent',
},
},
@ -63,7 +63,7 @@
</script>
{#await peoplePromise}
<div id="spinner" class="flex h-54 items-center justify-center -mb-4">
<div id="spinner" class="-mb-4 flex h-54 items-center justify-center">
<LoadingSpinner size="large" />
</div>
{:then people}
@ -72,14 +72,14 @@
? filterPeople(people, name)
: filterPeople(people, name).slice(0, numberOfPeople)}
<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 id="people-selection" class="-mb-4 max-h-60 overflow-y-auto immich-scrollbar">
<div class="flex w-full items-center justify-between gap-6">
<Text class="py-3" fontWeight="medium">{$t('people')}</Text>
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
</div>
<SingleGridRow
class="grid grid-auto-fill-20 gap-1 mt-2 overflow-y-auto immich-scrollbar space-between"
class="space-between mt-2 grid grid-auto-fill-20 gap-1 overflow-y-auto immich-scrollbar"
bind:itemCount={numberOfPeople}
>
{#each peopleList as person (person.id)}
@ -95,13 +95,13 @@
</SingleGridRow>
{#if showAllPeople || people.length > peopleList.length}
<div class="flex justify-center mt-2">
<div class="mt-2 flex justify-center">
<Button
color="primary"
variant="ghost"
shape="round"
leadingIcon={showAllPeople ? mdiClose : mdiArrowRight}
class="flex gap-2 place-items-center"
class="flex place-items-center gap-2"
onclick={() => (showAllPeople = !showAllPeople)}
>
{showAllPeople ? $t('collapse') : $t('see_all_people')}

View File

@ -69,7 +69,7 @@
</div>
</form>
<section class="flex flex-wrap pt-2 gap-1">
<section class="flex flex-wrap gap-1 pt-2">
{#each selectedTags ?? [] as tagId (tagId)}
{@const tag = tagMap[tagId]}
{#if tag}

View File

@ -15,7 +15,7 @@
<section>
<fieldset>
<Text class="mb-2" fontWeight="medium">{$t('search_type')}</Text>
<div class="flex flex-wrap gap-x-5 gap-y-2 my-2">
<div class="my-2 flex flex-wrap gap-x-5 gap-y-2">
{#if featureFlagsManager.value.smartSearch}
<RadioButton name="query-type" id="context-radio" label={$t('context')} bind:group={queryType} value="smart" />
{/if}

View File

@ -64,7 +64,7 @@
</script>
<div
class="border-2 rounded-2xl border-primary/20 mt-4 px-6 py-4 transition-all {isOpen
class="mt-4 rounded-2xl border-2 border-primary/20 px-6 py-4 transition-all {isOpen
? 'border-primary/60 shadow-md'
: ''}"
bind:this={accordionElement}
@ -76,7 +76,7 @@
class="flex w-full place-items-center justify-between text-start"
>
<div>
<div class="flex gap-2 place-items-center">
<div class="flex place-items-center gap-2">
{#if icon}
<Icon {icon} class="text-primary" size="24" aria-hidden />
{/if}
@ -86,7 +86,7 @@
</div>
{#if subtitleSnippet}{@render subtitleSnippet()}{:else}
<p class="text-sm dark:text-immich-dark-fg mt-1">{subtitle}</p>
<p class="mt-1 text-sm dark:text-immich-dark-fg">{subtitle}</p>
{/if}
</div>
@ -110,7 +110,7 @@
</button>
{#if isOpen}
<ul transition:slide={{ duration: 150 }} class="mb-2 ms-4">
<ul transition:slide={{ duration: 150 }} class="ms-4 mb-2">
{@render children?.()}
</ul>
{/if}

View File

@ -29,7 +29,7 @@
<div class="flex place-items-center justify-between">
<div>
<div class="flex h-6.5 place-items-center gap-1">
<label class="font-medium text-sm" for={title}>
<label class="text-sm font-medium" for={title}>
{title}
</label>
{#if isEdited}

View File

@ -81,7 +81,7 @@
<div class="mb-4 w-full">
<div class="flex place-items-center gap-1">
<label class="font-medium text-primary text-sm min-h-6" for={label}>{label}</label>
<label class="min-h-6 text-sm font-medium text-primary" for={label}>{label}</label>
{#if required}
<div class="text-red-400">*</div>
{/if}
@ -97,7 +97,7 @@
</div>
{#if description}
<p class="immich-form-label pb-2 text-sm" id="{label}-desc">
<p class="pb-2 text-sm immich-form-label" id="{label}-desc">
{description}
</p>
{:else}
@ -105,11 +105,11 @@
{/if}
{#if inputType !== SettingInputFieldType.PASSWORD}
<div class="flex place-items-center place-content-center gap-2">
<div class="flex place-content-center place-items-center gap-2">
{#if inputType === SettingInputFieldType.COLOR}
<input
bind:this={input}
class="immich-form-input w-full pb-2 rounded-none me-1"
class="me-1 immich-form-input w-full rounded-none pb-2"
aria-describedby={description ? `${label}-desc` : undefined}
aria-labelledby="{label}-label"
id={label}
@ -128,7 +128,7 @@
<input
bind:this={input}
class="immich-form-input w-full pb-2 min-w-[50px]"
class="immich-form-input w-full min-w-[50px] pb-2"
class:color-picker={inputType === SettingInputFieldType.COLOR}
aria-describedby={description ? `${label}-desc` : undefined}
aria-labelledby="{label}-label"

View File

@ -35,7 +35,7 @@
<div class="flex place-items-center justify-between">
<div class="me-2">
<div class="flex h-6.5 place-items-center gap-1">
<label class="font-medium text-primary text-sm" for={switchId}>
<label class="text-sm font-medium text-primary" for={switchId}>
{title}
</label>
{#if isEdited}

View File

@ -10,6 +10,6 @@
<PurchaseInfo />
<div class="mb-6 mt-2">
<div class="mt-2 mb-6">
<ServerStatus />
</div>

View File

@ -73,7 +73,7 @@
{#if authManager.isPurchased && authManager.preferences.purchase.showSupportBadge}
<button
onclick={() => goto(Route.userSettings({ isOpen: OpenQueryParam.PURCHASE_SETTINGS }))}
class="w-full mt-2"
class="mt-2 w-full"
type="button"
>
<SupporterBadge size="small" effect="always" />
@ -86,18 +86,18 @@
onmouseleave={() => (hoverButton = false)}
onfocus={onButtonHover}
onblur={() => (hoverButton = false)}
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 min-w-52 w-full"
class="mt-2 flex w-full min-w-52 place-content-center place-items-center justify-between rounded-lg border border-immich-primary/20 p-2 shadow-md dark:border-immich-dark-primary/10 dark:bg-immich-dark-primary/10"
>
<div class="flex justify-between w-full place-items-center place-content-center">
<div class="flex place-items-center place-content-center gap-1">
<div class="flex w-full place-content-center place-items-center justify-between">
<div class="flex place-content-center place-items-center gap-1">
<Logo variant="icon" size="tiny" />
<p class="flex text-primary font-medium">
<p class="flex font-medium text-primary">
{$t('purchase_button_buy_immich')}
</p>
</div>
<div>
<Icon icon={mdiInformationOutline} class="hidden sidebar:flex text-primary font-medium" size="18" />
<Icon icon={mdiInformationOutline} class="hidden font-medium text-primary sidebar:flex" size="18" />
</div>
</div>
</button>
@ -108,15 +108,15 @@
{#if showMessage}
<dialog
open
class="hidden sidebar:block w-125 absolute bottom-19 start-64 bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl shadow-2xl px-8 py-6"
class="absolute inset-s-64 bottom-19 hidden w-125 rounded-3xl border border-gray-200 bg-gray-50 px-8 py-6 text-black shadow-2xl sidebar:block dark:border-gray-800 dark:bg-immich-dark-gray dark:text-white"
transition:fade={{ duration: 150 }}
onmouseover={() => (hoverMessage = true)}
onmouseleave={() => (hoverMessage = false)}
onfocus={() => (hoverMessage = true)}
onblur={() => (hoverMessage = false)}
>
<div class="flex justify-between place-items-center">
<div class="h-10 w-10">
<div class="flex place-items-center justify-between">
<div class="size-10">
<Logo variant="icon" size="small" />
</div>
<IconButton
@ -133,11 +133,11 @@
/>
</div>
<h1 class="text-lg font-medium my-3 text-primary">
<h1 class="my-3 text-lg font-medium text-primary">
{$t('purchase_panel_title')}
</h1>
<div class="text-gray-800 dark:text-white my-4">
<div class="my-4 text-gray-800 dark:text-white">
<p>
{$t('purchase_panel_info_1')}
</p>

View File

@ -29,17 +29,17 @@
<a
href={Route.viewAlbum(album)}
title={album.albumName}
class="flex w-full place-items-center justify-between gap-4 rounded-e-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-subtle hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary ps-10 group-hover:sm:px-10 md:px-10"
class="flex w-full place-items-center justify-between gap-4 rounded-e-full py-3 ps-10 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-subtle hover:text-immich-primary group-hover:sm:px-10 md:px-10 dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary"
>
<div>
<div
class="h-6 w-6 bg-cover rounded bg-gray-200 dark:bg-gray-600"
class="size-6 rounded-sm bg-gray-200 bg-cover dark:bg-gray-600"
style={album.albumThumbnailAssetId
? `background-image:url('${getAssetMediaUrl({ id: album.albumThumbnailAssetId })}')`
: ''}
></div>
</div>
<div class="grow text-sm font-medium truncate">
<div class="grow truncate text-sm font-medium">
{album.albumName}
</div>
</a>

View File

@ -58,16 +58,16 @@
</script>
<div
class="text-sm flex md:flex ps-5 pe-1 place-items-center place-content-center justify-between min-w-52 overflow-hidden dark:text-immich-dark-fg"
class="flex min-w-52 place-content-center place-items-center justify-between overflow-hidden ps-5 pe-1 text-sm md:flex dark:text-immich-dark-fg"
>
{#if $connected}
<div class="flex gap-2 place-items-center place-content-center">
<div class="w-1.75 h-1.75 bg-green-500 rounded-full"></div>
<div class="flex place-content-center place-items-center gap-2">
<div class="size-1.75 rounded-full bg-green-500"></div>
<p class="dark:text-immich-gray">{$t('server_online')}</p>
</div>
{:else}
<div class="flex gap-2 place-items-center place-content-center">
<div class="w-1.75 h-1.75 bg-red-500 rounded-full"></div>
<div class="flex place-content-center place-items-center gap-2">
<div class="size-1.75 rounded-full bg-red-500"></div>
<p class="text-red-500">{$t('server_offline')}</p>
</div>
{/if}
@ -77,7 +77,7 @@
<button
type="button"
onclick={() => info && modalManager.show(ServerAboutModal, { versions, info })}
class="dark:text-immich-gray flex gap-1 place-items-center place-content-center"
class="flex place-content-center place-items-center gap-1 dark:text-immich-gray"
>
{#if isMain}
<Icon icon={mdiAlert} size="1.5em" color="#ffcc4d" /> {info?.sourceRef}
@ -96,17 +96,17 @@
href={releaseInfo.releaseUrl}
target="_blank"
rel="noopener noreferrer"
class="mt-3 p-2.5 ms-4 rounded-lg text-sm min-w-52 border border-gray-200/50 dark:border-gray-700/50 bg-white/50 dark:bg-gray-800/50 hover:border-immich-primary/40 dark:hover:border-immich-dark-primary/40 hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/5 transition-all duration-200 group block"
class="group ms-4 mt-3 block min-w-52 rounded-lg border border-gray-200/50 bg-white/50 p-2.5 text-sm transition-all duration-200 hover:border-immich-primary/40 hover:bg-immich-primary/5 dark:border-gray-700/50 dark:bg-gray-800/50 dark:hover:border-immich-dark-primary/40 dark:hover:bg-immich-dark-primary/5"
>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
<Icon icon={mdiNewBox} size="16" class="text-immich-primary dark:text-immich-dark-primary opacity-80" />
<Icon icon={mdiNewBox} size="16" class="text-immich-primary opacity-80 dark:text-immich-dark-primary" />
<Text size="tiny" fontWeight="medium" class="text-gray-700 dark:text-gray-300">
{releaseInfo.availableVersion}
</Text>
</div>
<span
class="text-[11px] text-gray-500 dark:text-gray-400 group-hover:text-immich-primary dark:group-hover:text-immich-dark-primary transition-colors opacity-70 group-hover:opacity-100"
class="text-[11px] text-gray-500 opacity-70 transition-colors group-hover:text-immich-primary group-hover:opacity-100 dark:text-gray-400 dark:group-hover:text-immich-dark-primary"
>
{$t('new_update')}!
</span>

Some files were not shown because too many files have changed in this diff Show More