fix: UI/UX consistency overhaul — fix 8 bugs, standardize design system

Bug fixes:
- Backup refresh no longer re-renders entire page (separate refreshing state)
- SSL cert button no longer flickers when no certs available
- Volume mode selector rewritten to use proper scope system (7 scopes)
- Navigation flicker eliminated when returning from env/volumes pages
- Logout button moved to sidebar footer near theme/locale controls
- Subdomain pattern now shows variable hint tooltip ({project}, {stage}, etc.)
- SSL certificate selector moved to Credentials page with auto-save
- Projects page now has search/filter by name, image, or registry

Consistency improvements:
- New Breadcrumb component replaces 5 inline implementations
- New IconArrowLeft, IconChevronDown components replace inline SVGs
- All inline spinners replaced with IconLoader component
- 10 semantic badge classes with dark mode variants in tokens.css
- Global disabled button cursor-not-allowed rule
- Raw inputs in auth page replaced with FormField components
- Missing aria-labels added to icon-only buttons
- Error panels standardized to use design tokens
This commit is contained in:
2026-04-04 21:34:36 +03:00
parent 27ec23921d
commit 216bd7e2db
23 changed files with 494 additions and 251 deletions
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import { IconChevronRight } from '$lib/components/icons';
interface BreadcrumbItem {
label: string;
href?: string;
}
interface Props {
items: BreadcrumbItem[];
}
const { items }: Props = $props();
</script>
<nav aria-label="Breadcrumb" class="flex items-center gap-1.5 text-sm text-[var(--text-tertiary)]">
{#each items as item, i}
{#if i > 0}
<IconChevronRight size={14} />
{/if}
{#if item.href}
<a href={item.href} class="hover:text-[var(--text-link)] transition-colors">{item.label}</a>
{:else}
<span class="text-[var(--text-secondary)]">{item.label}</span>
{/if}
{/each}
</nav>
@@ -0,0 +1,7 @@
<script lang="ts">
interface Props { size?: number; class?: string; }
const { size = 20, class: c = '' }: Props = $props();
</script>
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
<path d="M19 12H5" /><path d="m12 19-7-7 7-7" />
</svg>
@@ -0,0 +1,7 @@
<script lang="ts">
interface Props { size?: number; class?: string; }
const { size = 20, class: c = '' }: Props = $props();
</script>
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class={c} aria-hidden="true">
<polyline points="6 9 12 15 18 9" />
</svg>
+2
View File
@@ -48,3 +48,5 @@ export { default as IconRefresh } from './IconRefresh.svelte';
export { default as IconProxies } from './IconProxies.svelte';
export { default as IconEvents } from './IconEvents.svelte';
export { default as IconLogout } from './IconLogout.svelte';
export { default as IconArrowLeft } from './IconArrowLeft.svelte';
export { default as IconChevronDown } from './IconChevronDown.svelte';
+9 -1
View File
@@ -58,7 +58,9 @@
"imageLoadFailed": "Failed to load images",
"alreadyAdded": "Already added",
"portHelpText": "Auto-detected from EXPOSE if empty",
"healthcheckHelpText": "Auto-detected from image if empty"
"healthcheckHelpText": "Auto-detected from image if empty",
"searchPlaceholder": "Search projects by name, image, or registry...",
"noMatchingProjects": "No projects match your search."
},
"projectDetail": {
"deleteProject": "Delete Project",
@@ -162,6 +164,7 @@
"save": "Save",
"add": "Add",
"adding": "Adding...",
"scopeGuide": "Volume Scopes",
"noVolumes": "No volumes configured yet. Add one above.",
"volumeAdded": "Volume added",
"volumeUpdated": "Volume updated",
@@ -258,6 +261,11 @@
"dockerNetworkHelp": "Docker network for deployed containers",
"subdomainPattern": "Subdomain Pattern",
"subdomainPatternHelp": "Pattern for auto-generated subdomains",
"subdomainVarsTitle": "Available variables",
"varProject": "Project name",
"varStage": "Stage name",
"varTag": "Image tag",
"varPort": "Container port",
"pollingInterval": "Polling Interval (seconds)",
"pollingIntervalHelp": "How often to check registries for new tags (10-86400)",
"notificationUrl": "Notification URL",
+9 -1
View File
@@ -58,7 +58,9 @@
"imageLoadFailed": "Не удалось загрузить образы",
"alreadyAdded": "Уже добавлен",
"portHelpText": "Автоопределение из EXPOSE, если пусто",
"healthcheckHelpText": "Автоопределение из образа, если пусто"
"healthcheckHelpText": "Автоопределение из образа, если пусто",
"searchPlaceholder": "Поиск по имени, образу или реестру...",
"noMatchingProjects": "Проекты не найдены."
},
"projectDetail": {
"deleteProject": "Удалить проект",
@@ -162,6 +164,7 @@
"save": "Сохранить",
"add": "Добавить",
"adding": "Добавление...",
"scopeGuide": "Области видимости томов",
"noVolumes": "Тома ещё не настроены. Добавьте один выше.",
"volumeAdded": "Том добавлен",
"volumeUpdated": "Том обновлён",
@@ -258,6 +261,11 @@
"dockerNetworkHelp": "Docker-сеть для развёрнутых контейнеров",
"subdomainPattern": "Шаблон поддомена",
"subdomainPatternHelp": "Шаблон для автоматически генерируемых поддоменов",
"subdomainVarsTitle": "Доступные переменные",
"varProject": "Имя проекта",
"varStage": "Имя стадии",
"varTag": "Тег образа",
"varPort": "Порт контейнера",
"pollingInterval": "Интервал опроса (секунды)",
"pollingIntervalHelp": "Как часто проверять реестры на новые теги (10-86400)",
"notificationUrl": "URL уведомлений",
+33
View File
@@ -210,6 +210,13 @@
animation: button-press 150ms ease-in-out;
}
/* ── Disabled Buttons ────────────────────────────────────────────── */
button:disabled,
a[aria-disabled="true"] {
cursor: not-allowed;
}
/* ── Skeleton Loader ──────────────────────────────────────────────── */
.skeleton {
@@ -226,6 +233,32 @@
/* ── Toggle Switch ────────────────────────────────────────────────── */
/* ── Badge Tokens ────────────────────────────────────────────────── */
.badge-success { background: #ecfdf5; color: #047857; }
.badge-warning { background: #fffbeb; color: #b45309; }
.badge-danger { background: #fef2f2; color: #dc2626; }
.badge-info { background: #eff6ff; color: #2563eb; }
.badge-purple { background: #faf5ff; color: #7c3aed; }
.badge-cyan { background: #ecfeff; color: #0e7490; }
.badge-gray { background: #f3f4f6; color: #4b5563; }
.badge-amber { background: #fffbeb; color: #b45309; }
.badge-indigo { background: #eef2ff; color: #4f46e5; }
.badge-rose { background: #fff1f2; color: #e11d48; }
[data-theme="dark"] .badge-success { background: rgba(6, 78, 59, 0.3); color: #34d399; }
[data-theme="dark"] .badge-warning { background: rgba(120, 53, 15, 0.3); color: #fbbf24; }
[data-theme="dark"] .badge-danger { background: rgba(127, 29, 29, 0.3); color: #f87171; }
[data-theme="dark"] .badge-info { background: rgba(30, 58, 138, 0.3); color: #60a5fa; }
[data-theme="dark"] .badge-purple { background: rgba(76, 29, 149, 0.3); color: #a78bfa; }
[data-theme="dark"] .badge-cyan { background: rgba(14, 116, 144, 0.3); color: #22d3ee; }
[data-theme="dark"] .badge-gray { background: rgba(55, 65, 81, 0.5); color: #9ca3af; }
[data-theme="dark"] .badge-amber { background: rgba(120, 53, 15, 0.3); color: #fbbf24; }
[data-theme="dark"] .badge-indigo { background: rgba(67, 56, 202, 0.3); color: #818cf8; }
[data-theme="dark"] .badge-rose { background: rgba(159, 18, 57, 0.3); color: #fb7185; }
/* ── Toggle Switch ────────────────────────────────────────────────── */
.toggle-switch {
position: relative;
width: 2.75rem;