style(project): format Go with gofmt, set up Prettier for the frontend

Run gofmt -w across the backend, normalising the manually-aligned := blocks
to the gofmt standard. No code behaviour changes.

Add Prettier (+ prettier-plugin-svelte) to the frontend with the SvelteKit
default config (tabs, single quotes) so formatting is reproducible, then run
it over the whole tree. Add format / format:check npm scripts and a
.prettierignore (build output, generated schema.ts, static assets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 11:01:29 +03:00
parent f5f7db6c2a
commit 0e7890a465
70 changed files with 2753 additions and 1047 deletions
+3 -3
View File
@@ -8,7 +8,7 @@ export async function login(name: string, password: string): Promise<void> {
authStore.update((s) => ({
...s,
accessToken: tokens.access_token ?? null,
refreshToken: tokens.refresh_token ?? null,
refreshToken: tokens.refresh_token ?? null
}));
}
@@ -28,7 +28,7 @@ export async function refresh(): Promise<void> {
authStore.update((s) => ({
...s,
accessToken: tokens.access_token ?? null,
refreshToken: tokens.refresh_token ?? null,
refreshToken: tokens.refresh_token ?? null
}));
}
@@ -42,4 +42,4 @@ export function listSessions(params?: { offset?: number; limit?: number }): Prom
export function terminateSession(sessionId: number): Promise<void> {
return api.delete<void>(`/auth/sessions/${sessionId}`);
}
}
+22 -11
View File
@@ -18,7 +18,7 @@ export class ApiError extends Error {
public readonly status: number,
public readonly code: string,
message: string,
public readonly details?: Array<{ field?: string; message?: string }>,
public readonly details?: Array<{ field?: string; message?: string }>
) {
super(message);
this.name = 'ApiError';
@@ -38,7 +38,7 @@ async function refreshTokens(): Promise<void> {
const res = await fetch(`${BASE}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
body: JSON.stringify({ refresh_token: refreshToken })
});
if (!res.ok) {
@@ -50,7 +50,7 @@ async function refreshTokens(): Promise<void> {
authStore.update((s) => ({
...s,
accessToken: data.access_token ?? null,
refreshToken: data.refresh_token ?? null,
refreshToken: data.refresh_token ?? null
}));
}
@@ -64,7 +64,7 @@ function buildHeaders(init: RequestInit | undefined, accessToken: string | null)
async function request<T>(path: string, init?: RequestInit): Promise<T> {
let res = await fetch(BASE + path, {
...init,
headers: buildHeaders(init, get(authStore).accessToken),
headers: buildHeaders(init, get(authStore).accessToken)
});
if (res.status === 401) {
@@ -81,18 +81,27 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
res = await fetch(BASE + path, {
...init,
headers: buildHeaders(init, get(authStore).accessToken),
headers: buildHeaders(init, get(authStore).accessToken)
});
}
if (!res.ok) {
let body: { code?: string; message?: string; details?: Array<{ field?: string; message?: string }> } = {};
let body: {
code?: string;
message?: string;
details?: Array<{ field?: string; message?: string }>;
} = {};
try {
body = await res.json();
} catch {
// ignore parse failure
}
throw new ApiError(res.status, body.code ?? 'error', body.message ?? res.statusText, body.details);
throw new ApiError(
res.status,
body.code ?? 'error',
body.message ?? res.statusText,
body.details
);
}
if (res.status === 204) return undefined as T;
@@ -103,7 +112,7 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
export function uploadWithProgress<T>(
path: string,
formData: FormData,
onProgress: (pct: number) => void,
onProgress: (pct: number) => void
): Promise<T> {
return new Promise((resolve, reject) => {
const token = get(authStore).accessToken;
@@ -126,7 +135,9 @@ export function uploadWithProgress<T>(
let body: { code?: string; message?: string } = {};
try {
body = JSON.parse(xhr.responseText);
} catch { /* ignore */ }
} catch {
/* ignore */
}
reject(new ApiError(xhr.status, body.code ?? 'error', body.message ?? xhr.statusText));
}
};
@@ -146,5 +157,5 @@ export const api = {
request<T>(path, { method: 'PUT', body: JSON.stringify(body) }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
upload: <T>(path: string, formData: FormData) =>
request<T>(path, { method: 'POST', body: formData }),
};
request<T>(path, { method: 'POST', body: formData })
};
@@ -120,4 +120,4 @@
.btn.confirm.danger:hover {
background-color: color-mix(in srgb, var(--color-danger) 80%, #fff);
}
</style>
</style>
@@ -21,9 +21,7 @@
function nearViewport(): boolean {
if (!sentinel) return false;
const rect = sentinel.getBoundingClientRect();
return edge === 'bottom'
? rect.top <= window.innerHeight + MARGIN
: rect.bottom >= -MARGIN;
return edge === 'bottom' ? rect.top <= window.innerHeight + MARGIN : rect.bottom >= -MARGIN;
}
function maybeLoad() {
@@ -100,6 +98,8 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
</style>
@@ -33,8 +33,8 @@
api.get<TagOffsetPage>('/tags?limit=200&sort=name&order=asc'),
api.post<{ common_tag_ids: string[]; partial_tag_ids: string[] }>(
'/files/bulk/common-tags',
{ file_ids: fileIds },
),
{ file_ids: fileIds }
)
]);
allTags = tagsRes.items ?? [];
commonIds = new Set(commonRes.common_tag_ids ?? []);
@@ -53,16 +53,16 @@
allTags.filter(
(t) =>
assignedIds.has(t.id ?? '') &&
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase())),
),
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase()))
)
);
let availableTags = $derived(
allTags.filter(
(t) =>
!assignedIds.has(t.id ?? '') &&
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase())),
),
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase()))
)
);
function tagStyle(tag: Tag) {
@@ -132,8 +132,10 @@
class="tag assigned"
class:partial={isPartial}
style={tagStyle(tag)}
onclick={() => isPartial ? promotePartial(tag.id!) : remove(tag.id!)}
title={isPartial ? 'Partial — click to add to all files' : 'Click to remove from all files'}
onclick={() => (isPartial ? promotePartial(tag.id!) : remove(tag.id!))}
title={isPartial
? 'Partial — click to add to all files'
: 'Click to remove from all files'}
>
{tag.name}
{#if isPartial}
@@ -159,7 +161,12 @@
{#if search}
<button class="search-clear" onclick={() => (search = '')} aria-label="Clear search">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path
d="M2 2l10 10M12 2L2 12"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</button>
{/if}
@@ -347,4 +354,4 @@
color: var(--color-text-muted);
margin: 0;
}
</style>
</style>
@@ -22,7 +22,7 @@
selected = false,
selectionMode = false,
onTap,
onLongPress,
onLongPress
}: Props = $props();
let imgSrc = $state<string | null>(null);
@@ -34,7 +34,7 @@
let cancelled = false;
fetch(`/api/v1/files/${file.id}/thumbnail`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
headers: token ? { Authorization: `Bearer ${token}` } : {}
})
.then((res) => (res.ok ? res.blob() : null))
.then((blob) => {
@@ -111,7 +111,10 @@
data-file-index={index}
onpointerdown={onPointerDown}
onpointermove={onPointerMoveInternal}
onpointerup={() => { cancelPress(); didLongPress = false; }}
onpointerup={() => {
cancelPress();
didLongPress = false;
}}
onpointerleave={cancelPress}
oncontextmenu={(e) => e.preventDefault()}
onclick={onClick}
@@ -128,14 +131,27 @@
{#if selected}
<div class="check" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8.5" fill="rgba(0,0,0,0.55)" stroke="white" stroke-width="1"/>
<path d="M5 9l3 3 5-5" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="9" r="8.5" fill="rgba(0,0,0,0.55)" stroke="white" stroke-width="1" />
<path
d="M5 9l3 3 5-5"
stroke="white"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
{:else if selectionMode}
<div class="check" aria-hidden="true">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
<circle cx="9" cy="9" r="8.5" fill="rgba(0,0,0,0.35)" stroke="rgba(255,255,255,0.5)" stroke-width="1"/>
<circle
cx="9"
cy="9"
r="8.5"
fill="rgba(0,0,0,0.35)"
stroke="rgba(255,255,255,0.5)"
stroke-width="1"
/>
</svg>
</div>
{/if}
@@ -207,7 +223,11 @@
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
</style>
@@ -50,13 +50,11 @@
id: uid(),
name: f.name,
progress: 0,
status: 'uploading',
status: 'uploading'
}));
queue = [...queue, ...items];
await Promise.all(
files.map((file, i) => uploadOne(file, items[i].id)),
);
await Promise.all(files.map((file, i) => uploadOne(file, items[i].id)));
}
async function uploadOne(file: globalThis.File, itemId: string) {
@@ -64,10 +62,8 @@
fd.append('file', file);
try {
const result = await uploadWithProgress<ApiFile>(
'/files',
fd,
(pct) => updateItem(itemId, { progress: pct }),
const result = await uploadWithProgress<ApiFile>('/files', fd, (pct) =>
updateItem(itemId, { progress: pct })
);
updateItem(itemId, { status: 'done', progress: 100 });
onUploaded(result);
@@ -144,8 +140,14 @@
<div class="drop-overlay" aria-hidden="true">
<div class="drop-label">
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" aria-hidden="true">
<path d="M18 4v20M10 14l8-10 8 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 28h24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
<path
d="M18 4v20M10 14l8-10 8 10"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M6 28h24" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" />
</svg>
Drop files to upload
</div>
@@ -171,7 +173,11 @@
<ul class="upload-list">
{#each queue as item (item.id)}
<li class="upload-item" class:done={item.status === 'done'} class:error={item.status === 'error'}>
<li
class="upload-item"
class:done={item.status === 'done'}
class:error={item.status === 'error'}
>
<span class="item-name" title={item.name}>{item.name}</span>
<div class="item-right">
{#if item.status === 'uploading'}
@@ -180,8 +186,21 @@
</div>
<span class="pct">{item.progress}%</span>
{:else if item.status === 'done'}
<svg class="icon-ok" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-label="Done">
<path d="M3 8l4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg
class="icon-ok"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-label="Done"
>
<path
d="M3 8l4 4 6-6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{:else}
<span class="err-msg" title={item.error}>{item.error}</span>
@@ -243,8 +262,14 @@
}
@keyframes slide-up {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.panel-header {
@@ -348,4 +373,4 @@
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</style>
@@ -42,7 +42,7 @@
let dirty = $state(false);
let exifEntries = $derived(
file?.exif ? Object.entries(file.exif as Record<string, unknown>) : [],
file?.exif ? Object.entries(file.exif as Record<string, unknown>) : []
);
// ---- Load (re-runs whenever the file changes, i.e. paging) ----
@@ -88,7 +88,7 @@
const token = get(authStore).accessToken;
try {
const res = await fetch(`/api/v1/files/${id}/preview`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
if (res.ok && fileId === id) {
previewSrc = URL.createObjectURL(await res.blob());
@@ -129,13 +129,13 @@
(entries) => {
tagsVisible = entries[0]?.isIntersecting ?? false;
},
{ rootMargin: '200px' },
{ rootMargin: '200px' }
);
observer.observe(node);
return {
destroy() {
observer.disconnect();
},
}
};
}
@@ -158,10 +158,8 @@
try {
const updated = await api.patch<File>(`/files/${file.id}`, {
notes: notes.trim() || null,
content_datetime: contentDatetime
? new Date(contentDatetime).toISOString()
: undefined,
is_public: isPublic,
content_datetime: contentDatetime ? new Date(contentDatetime).toISOString() : undefined,
is_public: isPublic
});
file = updated;
dirty = false;
@@ -206,7 +204,13 @@
<div class="top-bar">
<button class="back-btn" onclick={onClose} aria-label="Back to files">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M12 4L6 10L12 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M12 4L6 10L12 16"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
<span class="filename">{file?.original_name ?? ''}</span>
@@ -230,7 +234,13 @@
aria-label="Previous file"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M11 3L5 9L11 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M11 3L5 9L11 15"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
{/if}
@@ -241,7 +251,13 @@
aria-label="Next file"
>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<path d="M7 3L13 9L7 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M7 3L13 9L7 15"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
{/if}
@@ -290,7 +306,10 @@
<button
class="toggle"
class:on={isPublic}
onclick={() => { isPublic = !isPublic; dirty = true; }}
onclick={() => {
isPublic = !isPublic;
dirty = true;
}}
role="switch"
aria-checked={isPublic}
aria-label="Public"
@@ -299,11 +318,7 @@
</button>
</section>
<button
class="save-btn"
onclick={save}
disabled={!dirty || saving}
>
<button class="save-btn" onclick={save} disabled={!dirty || saving}>
{saving ? 'Saving…' : 'Save changes'}
</button>
@@ -409,12 +424,7 @@
}
.preview-placeholder.shimmer {
background: linear-gradient(
90deg,
#111 25%,
#222 50%,
#111 75%
);
background: linear-gradient(90deg, #111 25%, #222 50%, #111 75%);
background-size: 200% 100%;
animation: shimmer 1.4s infinite;
}
@@ -445,8 +455,12 @@
background-color: rgba(0, 0, 0, 0.8);
}
.nav-prev { left: 10px; }
.nav-next { right: 10px; }
.nav-prev {
left: 10px;
}
.nav-next {
right: 10px;
}
/* ---- Metadata panel ---- */
.meta-panel {
@@ -465,7 +479,9 @@
padding-bottom: 10px;
}
.sep { opacity: 0.4; }
.sep {
opacity: 0.4;
}
.section {
padding: 10px 0;
@@ -577,7 +593,9 @@
cursor: pointer;
margin-top: 4px;
margin-bottom: 4px;
transition: background-color 0.15s, opacity 0.15s;
transition:
background-color 0.15s,
opacity 0.15s;
}
.save-btn:hover:not(:disabled) {
@@ -632,7 +650,11 @@
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
@@ -18,11 +18,7 @@
let search = $state('');
let tokens = $state<string[]>(parseDslFilter(value));
let tagNames = $derived(
new Map(
tags
.filter((t) => t.id && t.name)
.map((t) => [t.id as string, t.name as string]),
),
new Map(tags.filter((t) => t.id && t.name).map((t) => [t.id as string, t.name as string]))
);
$effect(() => {
@@ -36,9 +32,7 @@
});
let filteredTags = $derived(
search.trim()
? tags.filter((t) => t.name?.toLowerCase().includes(search.toLowerCase()))
: tags,
search.trim() ? tags.filter((t) => t.name?.toLowerCase().includes(search.toLowerCase())) : tags
);
function addToken(t: string) {
@@ -143,7 +137,11 @@
{#each filteredTags as tag (tag.id)}
<button
class="token tag-token"
style="background-color: {tag.color ? '#' + tag.color : tag.category_color ? '#' + tag.category_color : 'var(--color-tag-default)'}"
style="background-color: {tag.color
? '#' + tag.color
: tag.category_color
? '#' + tag.category_color
: 'var(--color-tag-default)'}"
onclick={() => addToken(`t=${tag.id}`)}
>
{tag.name}
@@ -214,7 +212,9 @@
font-weight: 600;
cursor: grab;
user-select: none;
transition: opacity 0.15s, outline 0.1s;
transition:
opacity 0.15s,
outline 0.1s;
outline: 2px solid transparent;
}
@@ -326,4 +326,4 @@
.btn-close:hover {
color: var(--color-text-primary);
}
</style>
</style>
@@ -26,14 +26,14 @@
allTags.filter(
(t) =>
!assignedIds.has(t.id) &&
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase())),
),
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase()))
)
);
let filteredAssigned = $derived(
search.trim()
? fileTags.filter((t) => t.name?.toLowerCase().includes(search.toLowerCase()))
: fileTags,
: fileTags
);
async function handleAdd(tagId: string) {
@@ -93,7 +93,12 @@
{#if search}
<button class="search-clear" onclick={() => (search = '')} aria-label="Clear search">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path
d="M2 2l10 10M12 2L2 12"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</button>
{/if}
@@ -239,4 +244,4 @@
color: var(--color-text-muted);
margin: 0;
}
</style>
</style>
@@ -23,7 +23,7 @@
onOrderToggle,
onFilterToggle,
onUpload,
onTrash,
onTrash
}: Props = $props();
</script>
@@ -39,8 +39,14 @@
{#if onUpload}
<button class="upload-btn icon-btn" onclick={onUpload} title="Upload files">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 2v9M4 6l4-4 4 4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 13h12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path
d="M8 2v9M4 6l4-4 4 4"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path d="M2 13h12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
</button>
{/if}
@@ -48,7 +54,13 @@
{#if onTrash}
<button class="icon-btn trash-btn" onclick={onTrash} title="Trash">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
<path d="M2 4h11M5 4V2.5h5V4M5.5 7v4.5M9.5 7v4.5M3 4l.8 9h7.4l.8-9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M2 4h11M5 4V2.5h5V4M5.5 7v4.5M9.5 7v4.5M3 4l.8 9h7.4l.8-9"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
{/if}
@@ -64,14 +76,30 @@
{/each}
</select>
<button class="icon-btn order-btn" onclick={onOrderToggle} title={order === 'asc' ? 'Ascending' : 'Descending'}>
<button
class="icon-btn order-btn"
onclick={onOrderToggle}
title={order === 'asc' ? 'Ascending' : 'Descending'}
>
{#if order === 'asc'}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M4 10L8 6L12 10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M4 10L8 6L12 10"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{:else}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M4 6L8 10L12 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/if}
</button>
@@ -83,7 +111,12 @@
title="Filter"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M2 4h12M4 8h8M6 12h4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path
d="M2 4h12M4 8h8M6 12h4"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</button>
</div>
@@ -173,4 +206,4 @@
color: var(--color-accent);
border-color: var(--color-accent);
}
</style>
</style>
@@ -22,8 +22,20 @@
<button class="count" onclick={() => selectionStore.exit()} title="Clear selection">
<span class="num">{$selectionCount}</span>
<span class="label">selected</span>
<svg class="close-icon" width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<svg
class="close-icon"
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
aria-hidden="true"
>
<path
d="M2 2l10 10M12 2L2 12"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</button>
@@ -51,8 +63,14 @@
}
@keyframes slide-up {
from { transform: translateY(12px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
from {
transform: translateY(12px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.row {
@@ -135,4 +153,4 @@
.delete:hover {
background-color: color-mix(in srgb, var(--color-danger) 15%, transparent);
}
</style>
</style>
@@ -53,4 +53,4 @@
button.badge:hover {
filter: brightness(1.15);
}
</style>
</style>
@@ -31,8 +31,8 @@
(t) =>
t.id !== tagId &&
!usedIds.has(t.id) &&
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase())),
),
(!search.trim() || t.name?.toLowerCase().includes(search.toLowerCase()))
)
);
function tagForId(id: string | undefined) {
@@ -47,7 +47,7 @@
const rule = await api.post<TagRule>(`/tags/${tagId}/rules`, {
then_tag_id: thenTagId,
is_active: true,
apply_to_existing: $appSettings.tagRuleApplyToExisting,
apply_to_existing: $appSettings.tagRuleApplyToExisting
});
onRulesChange([...rules, rule]);
search = '';
@@ -68,7 +68,7 @@
const body: Record<string, unknown> = { is_active: activating };
if (activating) body.apply_to_existing = $appSettings.tagRuleApplyToExisting;
const updated = await api.patch<TagRule>(`/tags/${tagId}/rules/${thenTagId}`, body);
onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r));
onRulesChange(rules.map((r) => (r.then_tag_id === thenTagId ? updated : r)));
} catch (e) {
error = e instanceof ApiError ? e.message : 'Failed to update rule';
} finally {
@@ -92,9 +92,7 @@
</script>
<div class="editor" class:busy>
<p class="desc">
When this tag is applied, also apply:
</p>
<p class="desc">When this tag is applied, also apply:</p>
{#if error}
<p class="error" role="alert">{error}</p>
@@ -120,20 +118,20 @@
>
{#if rule.is_active}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.5"/>
<circle cx="6" cy="6" r="2.5" fill="currentColor"/>
<circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.5" />
<circle cx="6" cy="6" r="2.5" fill="currentColor" />
</svg>
{:else}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.5"/>
<circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1.5" />
</svg>
{/if}
</button>
<button
class="remove-btn"
onclick={() => removeRule(rule.then_tag_id!)}
aria-label="Remove rule"
>×</button>
aria-label="Remove rule">×</button
>
</div>
{/each}
</div>
@@ -155,7 +153,12 @@
{#if search}
<button class="search-clear" onclick={() => (search = '')} aria-label="Clear search">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
<path d="M2 2l10 10M12 2L2 12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path
d="M2 2l10 10M12 2L2 12"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</button>
{/if}
@@ -326,4 +329,4 @@
color: var(--color-danger);
margin: 0;
}
</style>
</style>
+2 -2
View File
@@ -8,7 +8,7 @@ export interface AppSettings {
const DEFAULTS: AppSettings = {
fileLoadLimit: 100,
tagRuleApplyToExisting: false,
tagRuleApplyToExisting: false
};
function load(): AppSettings {
@@ -25,4 +25,4 @@ export const appSettings = writable<AppSettings>(load());
appSettings.subscribe((v) => {
if (browser) localStorage.setItem('app-settings', JSON.stringify(v));
});
});
+1 -1
View File
@@ -31,4 +31,4 @@ authStore.subscribe((state) => {
}
});
export const isAuthenticated = derived(authStore, ($auth) => !!$auth.accessToken);
export const isAuthenticated = derived(authStore, ($auth) => !!$auth.accessToken);
+3 -3
View File
@@ -8,7 +8,7 @@ interface SelectionState {
function createSelectionStore() {
const { subscribe, update, set } = writable<SelectionState>({
active: false,
ids: new Set(),
ids: new Set()
});
return {
@@ -55,11 +55,11 @@ function createSelectionStore() {
clear() {
set({ active: false, ids: new Set() });
},
}
};
}
export const selectionStore = createSelectionStore();
export const selectionCount = derived(selectionStore, ($s) => $s.ids.size);
export const selectionActive = derived(selectionStore, ($s) => $s.active);
export const selectionActive = derived(selectionStore, ($s) => $s.active);
+6 -6
View File
@@ -29,30 +29,30 @@ function makeSortStore<F extends string>(key: string, defaults: SortState<F>) {
},
toggleOrder() {
store.update((s) => ({ ...s, order: s.order === 'asc' ? 'desc' : 'asc' }));
},
}
};
}
export const fileSorting = makeSortStore<FileSortField>('sort:files', {
sort: 'created',
order: 'desc',
order: 'desc'
});
export const tagSorting = makeSortStore<TagSortField>('sort:tags', {
sort: 'created',
order: 'desc',
order: 'desc'
});
export type CategorySortField = 'name' | 'color' | 'created';
export const categorySorting = makeSortStore<CategorySortField>('sort:categories', {
sort: 'name',
order: 'asc',
order: 'asc'
});
export type PoolSortField = 'name' | 'created';
export const poolSorting = makeSortStore<PoolSortField>('sort:pools', {
sort: 'created',
order: 'desc',
});
order: 'desc'
});
+1 -1
View File
@@ -36,4 +36,4 @@ export function tokenLabel(token: string, tagNames: Map<string, string>): string
return tagNames.get(id) ?? token;
}
return token;
}
}
+1 -1
View File
@@ -14,4 +14,4 @@ export async function resetPwa(): Promise<void> {
// Hard reload so the page (and a fresh service worker, if still installed)
// re-fetches everything from the network instead of the now-cleared cache.
location.reload();
}
}