refactor: comprehensive consistency review — UI/UX, code quality, functional parity

Fix 19 issues across 3 priority tiers found during full-codebase review:

CRITICAL:
- Fix undefined --color-secondary CSS variable causing invisible UI elements
- Fix Google Photos command templates using nonexistent asset.originalFileName
- Fix scheduler template variable docs (tracker_name → schedule_name)
- Add missing admin guard on notification template update endpoint

HIGH:
- Fix 5 hardcoded English strings missing i18n (MultiEntitySelect, actions,
  settings, TelegramBotTab, users)
- Replace 17 raw <button> elements with shared <Button> component
- Replace 5 raw error divs with shared <ErrorBanner> component
- Refactor webhook handler duplication into shared _dispatch_webhook_event()
- Add 30+ provider-specific fields to TrackingConfig TypeScript type
- Add default TrackingConfig seeds for immich and google_photos
- Add provider-specific command variable docs (Gitea, Planka, NUT, GP, Webhook)

MEDIUM:
- Replace hardcoded hex colors and Tailwind classes with CSS variable tokens
- Remove dead code (unused imports, orphaned check_notification_tracker)
- Fix Svelte 5 patterns ($state for _prevProviderId, remove unnecessary as any)
- Fix inconsistent POST response shape (targets now returns full response)
- Fix Google Photos template dead asset.year branches, clarify album_url docs
This commit is contained in:
2026-03-31 23:27:35 +03:00
parent 6113a0039c
commit 6e51164f8e
29 changed files with 501 additions and 309 deletions
@@ -105,7 +105,7 @@
{/if}
<button type="button" class="mes-trigger" onclick={openPalette}>
<MdiIcon name="mdiPlus" size={14} />
<span>{(values || []).length === 0 ? placeholder : `${(values || []).length} selected`}</span>
<span>{(values || []).length === 0 ? placeholder : t('common.nSelected').replace('{0}', String((values || []).length))}</span>
<span class="mes-trigger-arrow"><MdiIcon name="mdiChevronDown" size={14} /></span>
</button>
</div>
@@ -120,7 +120,7 @@
<input
bind:this={inputEl}
bind:value={query}
placeholder="Search..."
placeholder={t('common.search')}
class="mes-input"
type="text"
autocomplete="off"
+4 -1
View File
@@ -876,7 +876,10 @@
"saveFailed": "Save failed",
"loadFailed": "Failed to load data",
"dismiss": "Dismiss",
"systemSuffix": " (System)"
"systemSuffix": " (System)",
"search": "Search...",
"nSelected": "{0} selected",
"unknown": "Unknown"
},
"templateSlot": {
"message_assets_added": "New assets added to album",
+4 -1
View File
@@ -876,7 +876,10 @@
"saveFailed": "Не удалось сохранить",
"loadFailed": "Не удалось загрузить данные",
"dismiss": "Закрыть",
"systemSuffix": " (Системный)"
"systemSuffix": " (Системный)",
"search": "Поиск...",
"nSelected": "{0} выбрано",
"unknown": "Неизвестно"
},
"templateSlot": {
"message_assets_added": "Новые файлы добавлены в альбом",
+39
View File
@@ -125,6 +125,45 @@ export interface TrackingConfig {
track_sharing_changed: boolean;
track_images: boolean;
track_videos: boolean;
// Gitea-specific
track_push?: boolean;
track_issue_opened?: boolean;
track_issue_closed?: boolean;
track_issue_commented?: boolean;
track_pr_opened?: boolean;
track_pr_closed?: boolean;
track_pr_merged?: boolean;
track_pr_commented?: boolean;
track_release_published?: boolean;
// Planka-specific
track_card_created?: boolean;
track_card_updated?: boolean;
track_card_moved?: boolean;
track_card_deleted?: boolean;
track_card_commented?: boolean;
track_comment_updated?: boolean;
track_board_created?: boolean;
track_board_updated?: boolean;
track_board_deleted?: boolean;
track_list_created?: boolean;
track_list_updated?: boolean;
track_list_deleted?: boolean;
track_attachment_created?: boolean;
track_card_label_added?: boolean;
track_task_completed?: boolean;
// Scheduler-specific
track_scheduled_message?: boolean;
// NUT-specific
track_ups_online?: boolean;
track_ups_on_battery?: boolean;
track_ups_low_battery?: boolean;
track_ups_battery_restored?: boolean;
track_ups_comms_lost?: boolean;
track_ups_comms_restored?: boolean;
track_ups_replace_battery?: boolean;
track_ups_overload?: boolean;
// Webhook-specific
track_webhook_received?: boolean;
notify_favorites_only: boolean;
include_tags: boolean;
include_asset_details: boolean;
+11 -13
View File
@@ -20,6 +20,8 @@
import { providerDefaultIcon } from '$lib/grid-items';
import RuleEditor from './RuleEditor.svelte';
import ExecutionHistory from './ExecutionHistory.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import Button from '$lib/components/Button.svelte';
import type { Action, ActionRule } from '$lib/types';
let allActions = $derived(actionsCache.items);
@@ -162,18 +164,17 @@
}
function statusColor(status: string): string {
if (status === 'success') return '#059669';
if (status === 'partial') return '#f59e0b';
if (status === 'failed') return '#ef4444';
if (status === 'success') return 'var(--color-success-fg)';
if (status === 'partial') return 'var(--color-warning-fg)';
if (status === 'failed') return 'var(--color-error-fg)';
return 'var(--color-muted-foreground)';
}
</script>
<PageHeader title={t('actions.title')} description={t('actions.description')}>
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('actions.addAction')}
</button>
</Button>
</PageHeader>
{#if !loaded}
@@ -192,9 +193,7 @@
{#if showForm}
<div in:slide={{ duration: 200 }}>
<Card class="mb-6">
{#if error}
<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>
{/if}
{#if error}<ErrorBanner message={error} />{/if}
<form onsubmit={save} class="space-y-3">
<div>
<label class="block text-sm font-medium mb-1">{t('actions.provider')}</label>
@@ -242,7 +241,7 @@
</label>
<label class="flex items-center gap-1 text-sm">
<input type="radio" name="schedule_type" value="cron" bind:group={form.schedule_type} class="accent-[var(--color-primary)]" />
Cron
{t('actions.cronMode')}
</label>
</div>
{#if form.schedule_type === 'interval'}
@@ -263,10 +262,9 @@
<label for="act-enabled" class="text-sm">{t('actions.enabled')}</label>
</div>
<button type="submit" disabled={submitting}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
<Button type="submit" disabled={submitting}>
{submitting ? t('common.loading') : (editing ? t('common.save') : t('actions.addAction'))}
</button>
</Button>
</form>
{#if editing}
+3 -4
View File
@@ -131,10 +131,9 @@
<input type="checkbox" bind:checked={emailForm.smtp_use_tls} />
{t('emailBot.useTls')}
</label>
<button type="submit" disabled={emailSubmitting}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
<Button type="submit" disabled={emailSubmitting}>
{emailSubmitting ? t('common.loading') : (editingEmail ? t('common.save') : t('emailBot.addBot'))}
</button>
</Button>
</form>
</Card>
{/if}
@@ -157,7 +156,7 @@
<span class="text-xs text-[var(--color-muted-foreground)] font-mono">{bot.email}</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{bot.smtp_host}:{bot.smtp_port}</span>
{#if bot.smtp_use_tls}
<span class="text-xs px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-500">TLS</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-success-bg)] text-[var(--color-success-fg)]">TLS</span>
{/if}
</div>
</div>
+9 -10
View File
@@ -303,10 +303,9 @@
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)] font-mono" />
</div>
{/if}
<button type="submit" disabled={submitting}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
<Button type="submit" disabled={submitting}>
{submitting ? t('common.loading') : (editing ? t('common.save') : t('telegramBot.addBot'))}
</button>
</Button>
</form>
</Card>
{/if}
@@ -329,8 +328,8 @@
{/if}
<!-- Mode badge -->
<span class="text-xs px-1.5 py-0.5 rounded font-mono {bot.update_mode === 'webhook'
? 'bg-blue-500/10 text-blue-500'
: 'bg-emerald-500/10 text-emerald-500'}">
? 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
: 'bg-[var(--color-success-bg)] text-[var(--color-success-fg)]'}">
{bot.update_mode === 'webhook' ? t('telegramBot.webhook') : t('telegramBot.polling')}
</span>
</div>
@@ -378,7 +377,7 @@
onclick={(e: MouseEvent) => copyChatId(e, chat.chat_id)}
title={t('telegramBot.clickToCopy')}
role="button" tabindex="0">
<span class="font-medium truncate">{chat.title || chat.username || 'Unknown'}</span>
<span class="font-medium truncate">{chat.title || chat.username || t('common.unknown')}</span>
<span style="text-align:center" class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chatTypeLabel(chat.type)}</span>
<span style="text-align:center" class="text-xs text-[var(--color-muted-foreground)]">{(chat.language_code || '—').toUpperCase()}</span>
<div style="justify-self:center" onclick={(e: MouseEvent) => e.stopPropagation()}>
@@ -399,7 +398,7 @@
</div>
<span style="text-align:center" class="text-xs text-[var(--color-muted-foreground)] font-mono">{chat.chat_id}</span>
<div style="justify-self:end" class="flex items-center gap-1">
<IconButton icon="mdiSend" title="Test message" size={14}
<IconButton icon="mdiSend" title={t('common.test')} size={14}
onclick={(e: MouseEvent) => testChat(e, bot.id, chat.chat_id)}
disabled={chatTesting[`${bot.id}_${chat.chat_id}`]} />
<IconButton icon="mdiDelete" title={t('common.delete')} size={14}
@@ -465,7 +464,7 @@
</div>
{#if bot.update_mode === 'polling'}
<span class="text-xs text-emerald-500 flex items-center gap-1">
<span class="text-xs text-[var(--color-success-fg)] flex items-center gap-1">
<MdiIcon name="mdiCheckCircle" size={14} />
{t('telegramBot.pollingActive')}
</span>
@@ -489,7 +488,7 @@
{/if}
</span>
{#if ws.last_error_message}
<span class="text-xs text-red-500">{t('telegramBot.webhookError')}: {ws.last_error_message}</span>
<span class="text-xs text-[var(--color-error-fg)]">{t('telegramBot.webhookError')}: {ws.last_error_message}</span>
{/if}
{:else}
<button onclick={() => loadWebhookStatus(bot.id)}
@@ -500,7 +499,7 @@
{/if}
{#if !settings.external_url && bot.update_mode === 'webhook'}
<span class="text-xs text-amber-500 flex items-center gap-1">
<span class="text-xs text-[var(--color-warning-fg)] flex items-center gap-1">
<MdiIcon name="mdiAlert" size={14} />
{t('telegramBot.noExternalDomain')}
</span>
@@ -19,6 +19,7 @@
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import type { CommandConfig } from '$lib/types';
function templateName(id: number | null): string {
@@ -161,7 +162,7 @@
{#if showForm}
<Card class="mb-6">
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
{#if error}<ErrorBanner message={error} />{/if}
<form onsubmit={saveConfig} class="space-y-4">
<div>
<label for="cfg-name" class="block text-sm font-medium mb-1">{t('commandConfig.name')}</label>
@@ -230,10 +231,9 @@
</div>
{/if}
<button type="submit" disabled={submitting}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
<Button type="submit" disabled={submitting}>
{submitting ? t('common.loading') : (editing ? t('common.save') : t('common.create'))}
</button>
</Button>
</form>
</Card>
{/if}
@@ -268,7 +268,7 @@
<span style="color: var(--color-primary);"><MdiIcon name={cfg.icon || 'mdiConsoleLine'} size={20} /></span>
<p class="font-medium">{cfg.name}</p>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)] font-mono">{cfg.provider_type}</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-500 font-mono">
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-success-bg)] text-[var(--color-success-fg)] font-mono">
{(cfg.enabled_commands || []).length} {t('commandConfig.commands')}
</span>
</div>
@@ -18,6 +18,7 @@
import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import { providerDefaultIcon } from '$lib/grid-items';
import Button from '$lib/components/Button.svelte';
import type { ServiceProvider, TelegramBot } from '$lib/types';
let allCmdTrackers = $state<any[]>([]);
@@ -186,10 +187,9 @@
</script>
<PageHeader title={t('commandTracker.title')} description={t('commandTracker.description')}>
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('commandTracker.newTracker')}
</button>
</Button>
</PageHeader>
{#if !loaded}<Loading />{:else}
@@ -217,10 +217,9 @@
<EntitySelect items={configItems} bind:value={form.command_config_id} placeholder={t('commandTracker.selectCommandConfig')} />
</div>
<button type="submit" disabled={submitting}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
<Button type="submit" disabled={submitting}>
{submitting ? t('common.loading') : (editing ? t('common.save') : t('common.create'))}
</button>
</Button>
</form>
</Card>
{/if}
@@ -257,8 +256,8 @@
<CrossLink href="/providers" icon="mdiServer" label={providerName(trk.provider_id)} entityId={trk.provider_id} />
<CrossLink href="/command-configs" icon="mdiCog" label={configName(trk.command_config_id)} entityId={trk.command_config_id} />
<span class="text-xs px-1.5 py-0.5 rounded font-mono {trk.enabled
? 'bg-emerald-500/10 text-emerald-500'
: 'bg-red-500/10 text-red-500'}">
? 'bg-[var(--color-success-bg)] text-[var(--color-success-fg)]'
: 'bg-[var(--color-error-bg)] text-[var(--color-error-fg)]'}">
{trk.enabled ? t('commandTracker.enabled') : t('commandTracker.disabled')}
</span>
</div>
@@ -293,7 +292,7 @@
<div class="flex items-center gap-2">
<MdiIcon name="mdiRobot" size={14} />
<CrossLink href="/bots?tab=telegram" icon="mdiRobot" label={listener.name || listener.listener_type} entityId={listener.listener_id} />
<span class="text-xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-500 font-mono">{listener.listener_type}</span>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-primary)]/10 text-[var(--color-primary)] font-mono">{listener.listener_type}</span>
</div>
<IconButton icon="mdiClose" title={t('commandTracker.removeListener')} size={14}
onclick={() => removeListener(trk.id, listener.id)} variant="danger" />
@@ -307,10 +306,9 @@
<div class="flex-1">
<EntitySelect items={botItems} bind:value={newListenerBotId[trk.id]} placeholder={t('commandTracker.selectBot')} />
</div>
<button onclick={() => addListener(trk.id)} disabled={!newListenerBotId[trk.id] || addingListener[trk.id]}
class="text-xs px-3 py-1 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md hover:opacity-90 disabled:opacity-50">
<Button size="sm" onclick={() => addListener(trk.id)} disabled={!newListenerBotId[trk.id] || addingListener[trk.id]}>
{addingListener[trk.id] ? t('common.loading') : t('commandTracker.addListener')}
</button>
</Button>
</div>
</div>
{/if}
@@ -131,7 +131,7 @@
try { collections = await api(`/providers/${form.provider_id}/collections`); } catch (e) { console.warn('Failed to load collections:', e); collections = []; }
}
let _prevProviderId = 0;
let _prevProviderId = $state(0);
$effect(() => {
if (showForm && form.provider_id && form.provider_id !== _prevProviderId) {
_prevProviderId = form.provider_id;
@@ -146,8 +146,8 @@
name: trk.name, icon: trk.icon || '', provider_id: trk.provider_id,
collection_ids: [...(trk.collection_ids || [])],
scan_interval: trk.scan_interval, batch_duration: trk.batch_duration ?? 0,
default_tracking_config_id: (trk as any).default_tracking_config_id || 0,
default_template_config_id: (trk as any).default_template_config_id || 0,
default_tracking_config_id: trk.default_tracking_config_id ?? 0,
default_template_config_id: trk.default_template_config_id ?? 0,
filters: trk.filters || {},
};
previousCollectionIds = [...(trk.collection_ids || [])];
+7 -7
View File
@@ -285,19 +285,19 @@
transition: all 0.3s ease;
}
.health-dot.online {
background: #059669;
box-shadow: 0 0 8px rgba(5, 150, 105, 0.4);
background: var(--color-success-fg);
box-shadow: 0 0 8px color-mix(in srgb, var(--color-success-fg) 40%, transparent);
}
.health-dot.offline {
background: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.3);
background: var(--color-error-fg);
box-shadow: 0 0 8px color-mix(in srgb, var(--color-error-fg) 30%, transparent);
}
.health-dot.checking {
background: #f59e0b;
background: var(--color-warning-fg);
animation: pulseCheck 1.5s ease-in-out infinite;
}
@keyframes pulseCheck {
0%, 100% { box-shadow: 0 0 4px rgba(245, 158, 11, 0.3); }
50% { box-shadow: 0 0 12px rgba(245, 158, 11, 0.6); }
0%, 100% { box-shadow: 0 0 4px color-mix(in srgb, var(--color-warning-fg) 30%, transparent); }
50% { box-shadow: 0 0 12px color-mix(in srgb, var(--color-warning-fg) 60%, transparent); }
}
</style>
@@ -20,7 +20,8 @@
loading = true;
try {
logs = await api(`/providers/${providerId}/webhook-logs`);
} catch {
} catch (e) {
console.error('Failed to load webhook logs', e);
logs = [];
}
loading = false;
@@ -30,7 +31,7 @@
try {
await api(`/providers/${providerId}/webhook-logs`, { method: 'DELETE' });
logs = [];
} catch { /* ignore */ }
} catch (e) { console.error('Failed to clear webhook logs', e); }
showClearConfirm = false;
}
@@ -39,9 +40,9 @@
}
function statusColor(status: string): string {
if (status === 'matched') return '#059669';
if (status === 'unmatched') return '#f59e0b';
return '#ef4444';
if (status === 'matched') return 'var(--color-success-fg)';
if (status === 'unmatched') return 'var(--color-warning-fg)';
return 'var(--color-error-fg)';
}
function statusIcon(status: string): string {
@@ -71,7 +72,7 @@
<div class="flex items-center gap-2">
<h3 class="text-sm font-semibold">{t('webhookLogs.title')}</h3>
{#if logs.length > 0}
<span class="text-xs px-1.5 py-0.5 rounded-full bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)]">{logs.length}</span>
<span class="text-xs px-1.5 py-0.5 rounded-full bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{logs.length}</span>
{/if}
</div>
{#if logs.length > 0}
@@ -99,7 +100,7 @@
<div class="flex items-center gap-2">
<MdiIcon name={expandedId === log.id ? 'mdiChevronDown' : 'mdiChevronRight'} size={14} />
<span class="text-xs text-[var(--color-muted-foreground)] tabular-nums">{formatTime(log.created_at)}</span>
<span class="text-xs font-mono px-1 py-0.5 rounded bg-[var(--color-secondary)]">{log.method}</span>
<span class="text-xs font-mono px-1 py-0.5 rounded bg-[var(--color-muted)]">{log.method}</span>
<span class="flex items-center gap-1" style="color: {statusColor(log.status)};">
<MdiIcon name={statusIcon(log.status)} size={14} />
<span class="text-xs font-medium">{statusLabel(log.status)}</span>
+4 -4
View File
@@ -8,6 +8,7 @@
import MdiIcon from '$lib/components/MdiIcon.svelte';
import Hint from '$lib/components/Hint.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import Button from '$lib/components/Button.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
let loaded = $state(false);
@@ -67,7 +68,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium mb-1">{t('settings.webhookSecret')}<Hint text={t('settings.webhookSecretHint')} /></label>
<input bind:value={settings.telegram_webhook_secret} type="password" placeholder="optional"
<input bind:value={settings.telegram_webhook_secret} type="password" placeholder={t('providers.optional')}
class="w-full px-3 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)] font-mono" />
</div>
<div>
@@ -78,9 +79,8 @@
</div>
</Card>
<button onclick={save} disabled={saving}
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
<Button onclick={save} disabled={saving}>
{saving ? t('common.loading') : t('common.save')}
</button>
</Button>
</div>
{/if}
@@ -9,6 +9,7 @@
import MultiEntitySelect from '$lib/components/MultiEntitySelect.svelte';
import type { EntityItem } from '$lib/components/EntitySelect.svelte';
import type { GridItem } from '$lib/components/IconGridSelect.svelte';
import Button from '$lib/components/Button.svelte';
interface Props {
form: {
@@ -184,7 +185,7 @@
<label class="flex items-center gap-2 text-sm"><input type="checkbox" bind:checked={form.ai_captions} /> {t('targets.aiCaptions')}<Hint text={t('hints.aiCaptions')} /></label>
{/if}
<button type="submit" disabled={submitting} class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">{submitting ? t('common.loading') : (editing ? t('common.save') : t('targets.create'))}</button>
<Button type="submit" disabled={submitting}>{submitting ? t('common.loading') : (editing ? t('common.save') : t('targets.create'))}</Button>
</form>
</Card>
</div>
@@ -22,6 +22,8 @@
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import Button from '$lib/components/Button.svelte';
import type { TemplateConfig } from '$lib/types';
let allTemplateConfigs = $derived(templateConfigsCache.items);
@@ -263,10 +265,9 @@
</script>
<PageHeader title={t('templateConfig.title')} description={t('templateConfig.description')}>
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('templateConfig.newConfig')}
</button>
</Button>
</PageHeader>
{#if !loaded}<Loading />{:else}
@@ -274,7 +275,7 @@
{#if showForm}
<div in:slide={{ duration: 200 }}>
<Card class="mb-6">
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
{#if error}<ErrorBanner message={error} />{/if}
<form onsubmit={save} class="space-y-5">
<div>
<label for="tpc-name" class="block text-sm font-medium mb-1">{t('templateConfig.name')}</label>
@@ -391,9 +392,9 @@
{/if}
{/each}
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button type="submit">
{editing ? t('common.save') : t('common.create')}
</button>
</Button>
</form>
</Card>
</div>
@@ -19,6 +19,8 @@
import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import { getDescriptor, buildTrackingFormDefaults } from '$lib/providers';
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import Button from '$lib/components/Button.svelte';
import type { TrackingConfig } from '$lib/types';
/** Grid-select item source lookup — maps descriptor string name to actual function. */
@@ -83,10 +85,9 @@
</script>
<PageHeader title={t('trackingConfig.title')} description={t('trackingConfig.description')}>
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button size="sm" onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}>
{showForm ? t('common.cancel') : t('trackingConfig.newConfig')}
</button>
</Button>
</PageHeader>
{#if !loaded}<Loading />{:else}
@@ -94,7 +95,7 @@
{#if showForm}
<div in:slide={{ duration: 200 }}>
<Card class="mb-6">
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
{#if error}<ErrorBanner message={error} />{/if}
<form onsubmit={save} class="space-y-5">
<div>
<label for="tc-name" class="block text-sm font-medium mb-1">{t('trackingConfig.name')}</label>
@@ -194,9 +195,9 @@
</Card>
{/if}
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button type="submit">
{editing ? t('common.save') : t('common.create')}
</button>
</Button>
</form>
</Card>
</div>
+9 -8
View File
@@ -12,6 +12,8 @@
import EmptyState from '$lib/components/EmptyState.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
import Button from '$lib/components/Button.svelte';
import type { User } from '$lib/types';
const auth = getAuth();
@@ -67,17 +69,16 @@
</script>
<PageHeader title={t('users.title')} description={t('users.description')}>
<button onclick={() => showForm = !showForm}
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button size="sm" onclick={() => showForm = !showForm}>
{showForm ? t('users.cancel') : t('users.addUser')}
</button>
</Button>
</PageHeader>
{#if !loaded}<Loading />{:else}
{#if showForm}
<Card class="mb-6">
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
{#if error}<ErrorBanner message={error} />{/if}
<form onsubmit={create} class="space-y-3">
<div>
<label for="usr-name" class="block text-sm font-medium mb-1">{t('users.username')}</label>
@@ -94,7 +95,7 @@
<option value="admin">{t('users.roleAdmin')}</option>
</select>
</div>
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">{t('users.create')}</button>
<Button type="submit">{t('users.create')}</Button>
</form>
</Card>
{/if}
@@ -110,7 +111,7 @@
<div class="flex items-center justify-between">
<div>
<p class="font-medium">{user.username}</p>
<p class="text-sm text-[var(--color-muted-foreground)]">{user.role} · {t('users.joined')} {new Date(user.created_at).toLocaleDateString()}</p>
<p class="text-sm text-[var(--color-muted-foreground)]">{user.role === 'admin' ? t('users.roleAdmin') : t('users.roleUser')} · {t('users.joined')} {new Date(user.created_at).toLocaleDateString()}</p>
</div>
<div class="flex items-center gap-1">
{#if user.id !== auth.user?.id}
@@ -137,9 +138,9 @@
{#if resetMsg}
<p class="text-sm {resetSuccess ? 'text-[var(--color-success-fg)]' : 'text-[var(--color-error-fg)]'}">{resetMsg}</p>
{/if}
<button type="submit" class="w-full py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
<Button type="submit" class="w-full">
{t('common.save')}
</button>
</Button>
</form>
</Modal>