wapadil Claude commited on
Commit
20a1f08
·
1 Parent(s): 35c5dfd

[APPLE HIG UX] 完整Apple HIG设计系统重构与用户体验优化

Browse files

核心优化:
• 实现完整Apple HIG设计系统 - SF字体栈、iOS色彩、8pt网格
• 升级材质系统 - 22px模糊、180%饱和度、适应性透明度
• 全新通知系统 - 顶部Banner + 右下Toast,支持持久化错误通知
• 增强进度反馈 - 环形进度条、骨架屏、智能状态管理
• 优化交互动效 - prefers-reduced-motion支持、44pt触控目标
• 改进可访问性 - ARIA状态、语义化标签、键盘导航

技术实现:
- CSS: color-mix()自适应主题、backdrop-filter材质效果
- JS: Toast/Banner通知系统、增强进度状态管理
- HTML: 语义化通知容器、ARIA可访问性增强

📱 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. static/script.js +165 -0
  2. static/style.css +423 -53
  3. templates/index.html +8 -2
static/script.js CHANGED
@@ -5,6 +5,171 @@ let generationHistory = [];
5
  let currentGeneration = null;
6
  let activeTab = 'current';
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  // Initialize local storage
9
  const HISTORY_KEY = 'seedream_generation_history';
10
 
 
5
  let currentGeneration = null;
6
  let activeTab = 'current';
7
 
8
+ // ============================= //
9
+ // Apple HIG Enhanced Notifications & Progress System //
10
+ // ============================= //
11
+
12
+ // Toast notification system
13
+ function showToast(message, type = 'info', duration = 5000, actions = null) {
14
+ const toasts = document.getElementById('toasts');
15
+ const toast = document.createElement('div');
16
+ toast.className = `toast ${type}`;
17
+
18
+ const toastId = 'toast-' + Date.now();
19
+ toast.id = toastId;
20
+
21
+ let actionButtons = '';
22
+ if (actions) {
23
+ actionButtons = actions.map(action =>
24
+ `<button class="toast-action" onclick="${action.onclick}">${action.text}</button>`
25
+ ).join('');
26
+ }
27
+
28
+ toast.innerHTML = `
29
+ <div class="toast-header">
30
+ <span>${getToastIcon(type)} ${getToastTitle(type)}</span>
31
+ <button class="toast-close" onclick="hideToast('${toastId}')" aria-label="关闭通知">×</button>
32
+ </div>
33
+ <div class="toast-body">
34
+ ${message}
35
+ ${actionButtons ? `<div class="toast-actions">${actionButtons}</div>` : ''}
36
+ </div>
37
+ `;
38
+
39
+ toasts.appendChild(toast);
40
+
41
+ // Trigger show animation
42
+ requestAnimationFrame(() => {
43
+ toast.classList.add('show');
44
+ });
45
+
46
+ // Auto-hide unless it's an error
47
+ if (type !== 'error' && duration > 0) {
48
+ setTimeout(() => hideToast(toastId), duration);
49
+ }
50
+
51
+ return toastId;
52
+ }
53
+
54
+ function hideToast(toastId) {
55
+ const toast = document.getElementById(toastId);
56
+ if (toast) {
57
+ toast.classList.remove('show');
58
+ setTimeout(() => {
59
+ if (toast.parentNode) {
60
+ toast.parentNode.removeChild(toast);
61
+ }
62
+ }, 300);
63
+ }
64
+ }
65
+
66
+ function getToastIcon(type) {
67
+ const icons = {
68
+ success: '✓',
69
+ error: '⚠',
70
+ warning: '⚡',
71
+ info: 'ⓘ'
72
+ };
73
+ return icons[type] || icons.info;
74
+ }
75
+
76
+ function getToastTitle(type) {
77
+ const titles = {
78
+ success: '成功',
79
+ error: '错误',
80
+ warning: '警告',
81
+ info: '提示'
82
+ };
83
+ return titles[type] || titles.info;
84
+ }
85
+
86
+ // Banner notification system (for top-level status)
87
+ function showBanner(message, type = 'info', duration = 4000) {
88
+ const banner = document.getElementById('banner');
89
+ banner.textContent = message;
90
+ banner.className = `banner ${type}`;
91
+ banner.hidden = false;
92
+ banner.classList.add('show');
93
+
94
+ if (duration > 0) {
95
+ setTimeout(() => {
96
+ banner.classList.remove('show');
97
+ setTimeout(() => {
98
+ banner.hidden = true;
99
+ }, 300);
100
+ }, duration);
101
+ }
102
+ }
103
+
104
+ // Enhanced progress feedback for generate button
105
+ function setGenerateButtonProgress(isLoading, text = '生成图像') {
106
+ const btn = document.getElementById('generateBtn');
107
+ const spinner = btn.querySelector('.spinner');
108
+ const progressRing = btn.querySelector('.progress-ring');
109
+ const btnText = btn.querySelector('.btn-text');
110
+
111
+ if (isLoading) {
112
+ btn.setAttribute('aria-busy', 'true');
113
+ btn.disabled = true;
114
+ spinner.style.display = 'block';
115
+ progressRing.style.display = 'block';
116
+ btnText.textContent = text;
117
+ } else {
118
+ btn.setAttribute('aria-busy', 'false');
119
+ btn.disabled = false;
120
+ spinner.style.display = 'none';
121
+ progressRing.style.display = 'none';
122
+ btnText.textContent = '生成图像';
123
+ }
124
+ }
125
+
126
+ // Enhanced status message with better UX
127
+ function showStatus(message, type = 'info', persistent = false) {
128
+ const statusEl = document.getElementById('statusMessage');
129
+ statusEl.textContent = message;
130
+ statusEl.className = `status-message ${type}`;
131
+ statusEl.style.display = 'block';
132
+
133
+ if (!persistent && type !== 'error') {
134
+ setTimeout(() => {
135
+ statusEl.style.display = 'none';
136
+ }, type === 'success' ? 3000 : 5000);
137
+ }
138
+ }
139
+
140
+ // Progress logs with collapsible details
141
+ function addProgressLog(message, type = 'info') {
142
+ const logs = document.getElementById('progressLogs');
143
+ if (!logs.classList.contains('active')) {
144
+ logs.classList.add('active');
145
+ }
146
+
147
+ const entry = document.createElement('div');
148
+ entry.className = `log-entry ${type}`;
149
+ entry.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
150
+
151
+ logs.appendChild(entry);
152
+ logs.scrollTop = logs.scrollHeight;
153
+ }
154
+
155
+ function clearProgressLogs() {
156
+ const logs = document.getElementById('progressLogs');
157
+ logs.innerHTML = '';
158
+ logs.classList.remove('active');
159
+ }
160
+
161
+ // Skeleton loader for preview areas
162
+ function showPreviewSkeleton(containerId, count = 1) {
163
+ const container = document.getElementById(containerId);
164
+ container.innerHTML = '';
165
+
166
+ for (let i = 0; i < count; i++) {
167
+ const skeleton = document.createElement('div');
168
+ skeleton.className = 'preview-skeleton skeleton';
169
+ container.appendChild(skeleton);
170
+ }
171
+ }
172
+
173
  // Initialize local storage
174
  const HISTORY_KEY = 'seedream_generation_history';
175
 
static/style.css CHANGED
@@ -5,47 +5,55 @@
5
  --safe-bottom: max(16px, env(safe-area-inset-bottom));
6
  --safe-top: max(16px, env(safe-area-inset-top));
7
 
 
 
 
 
 
 
 
8
  /* Professional Design System Colors */
9
- --bg: #fafbfc;
10
- --surface: #ffffff;
11
- --surface-secondary: #f6f8fa;
12
- --surface-tertiary: #f1f3f4;
13
-
14
- /* Text hierarchy */
15
- --text-primary: #1c1e21;
16
- --text-secondary: #656d76;
17
- --text-tertiary: #8c959f;
18
- --text-muted: #afb8c1;
19
-
20
- /* Border system */
21
- --border-light: #e1e5e9;
22
- --border-medium: #d0d7de;
23
- --border-strong: #a2a9b1;
24
-
25
- /* Brand and interaction */
26
- --brand-primary: #6366f1;
27
- --brand-secondary: #8b5cf6;
28
- --brand-tertiary: #a855f7;
29
- --brand-bg: #f8faff;
30
- --brand-hover: #5145e5;
31
-
32
- /* Semantic colors */
33
- --success: #22c55e;
34
- --success-bg: #f0fdf4;
35
- --danger: #ef4444;
36
- --danger-bg: #fef2f2;
37
- --warning: #f59e0b;
38
- --warning-bg: #fffbeb;
39
- --info: #3b82f6;
40
- --info-bg: #eff6ff;
41
-
42
- /* Shadows */
43
- --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
44
- --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
45
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
46
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
47
-
48
- /* Grid system */
 
49
  --spacing-1: 4px;
50
  --spacing-2: 8px;
51
  --spacing-3: 12px;
@@ -56,12 +64,13 @@
56
  --spacing-10: 40px;
57
  --spacing-12: 48px;
58
 
59
- /* Border radius */
60
- --radius-xs: 2px;
61
- --radius-sm: 4px;
62
- --radius-md: 6px;
63
- --radius-lg: 8px;
64
  --radius-xl: 12px;
 
65
  }
66
 
67
  @media (prefers-color-scheme: dark) {
@@ -109,20 +118,37 @@
109
  }
110
 
111
  body {
112
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
113
  background: var(--bg);
114
  color: var(--text-primary);
115
  min-height: 100vh;
116
  min-height: 100svh;
117
  min-height: 100dvh;
118
  overflow: auto;
119
- line-height: 1.5;
120
  -webkit-font-smoothing: antialiased;
121
  -moz-osx-font-smoothing: grayscale;
122
- font-size: 14px;
123
  letter-spacing: -0.01em;
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  /* Main App Container - Professional Layout */
127
  .app-container {
128
  display: grid;
@@ -909,7 +935,7 @@ body {
909
  visibility: visible;
910
  }
911
 
912
- /* Drawer container */
913
  .drawer {
914
  position: fixed;
915
  top: 0;
@@ -919,10 +945,10 @@ body {
919
  transform: translateX(-100%);
920
  transition: transform 0.35s ease;
921
  z-index: 1000;
922
- backdrop-filter: blur(20px) saturate(180%);
923
- -webkit-backdrop-filter: blur(20px) saturate(180%);
924
- background: color-mix(in oklab, var(--surface) 85%, transparent);
925
- border-right: 1px solid color-mix(in oklab, var(--border-light) 50%, transparent);
926
  box-shadow: var(--shadow-strong);
927
  overflow: hidden;
928
  display: flex;
@@ -1685,4 +1711,348 @@ select:focus-visible {
1685
  .tab-btn:focus-visible {
1686
  outline: 2px solid var(--brand-primary);
1687
  outline-offset: -2px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1688
  }
 
5
  --safe-bottom: max(16px, env(safe-area-inset-bottom));
6
  --safe-top: max(16px, env(safe-area-inset-top));
7
 
8
+ /* Apple HIG Design System - Typography */
9
+ --font-sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
10
+ --size-title: clamp(22px, 3.2vw, 28px); /* 对应 SF Pro Display */
11
+ --size-sub: clamp(17px, 2vw, 20px); /* 小标题/按钮 */
12
+ --size-body: 16px; /* ≥11pt */
13
+ --size-caption: 13px;
14
+
15
  /* Professional Design System Colors */
16
+ --bg: Canvas;
17
+ --surface: Canvas;
18
+ --surface-secondary: color-mix(in oklab, Canvas 85%, CanvasText 15%);
19
+ --surface-tertiary: color-mix(in oklab, Canvas 78%, CanvasText 22%);
20
+
21
+ /* Text hierarchy with adaptive contrast */
22
+ --text-primary: CanvasText;
23
+ --text-secondary: color-mix(in oklab, CanvasText 75%, Canvas 25%);
24
+ --text-tertiary: color-mix(in oklab, CanvasText 55%, Canvas 45%);
25
+ --text-muted: color-mix(in oklab, CanvasText 40%, Canvas 60%);
26
+
27
+ /* Border system with adaptive opacity */
28
+ --border-light: color-mix(in oklab, CanvasText 18%, transparent);
29
+ --border-medium: color-mix(in oklab, CanvasText 28%, transparent);
30
+ --border-strong: color-mix(in oklab, CanvasText 45%, transparent);
31
+
32
+ /* Brand and interaction - iOS Blue */
33
+ --brand-primary: #007AFF;
34
+ --brand-secondary: #5AC8FA;
35
+ --brand-tertiary: #64D2FF;
36
+ --brand-bg: color-mix(in oklab, #007AFF 12%, Canvas 88%);
37
+ --brand-hover: #005FCC;
38
+
39
+ /* Semantic colors - iOS system colors */
40
+ --success: #34C759;
41
+ --success-bg: color-mix(in oklab, #34C759 12%, Canvas 88%);
42
+ --danger: #FF3B30;
43
+ --danger-bg: color-mix(in oklab, #FF3B30 12%, Canvas 88%);
44
+ --warning: #FF9500;
45
+ --warning-bg: color-mix(in oklab, #FF9500 12%, Canvas 88%);
46
+ --info: #007AFF;
47
+ --info-bg: color-mix(in oklab, #007AFF 12%, Canvas 88%);
48
+
49
+ /* Apple Materials & Depth */
50
+ --shadow-xs: 0 1px 2px 0 color-mix(in oklab, CanvasText 8%, transparent);
51
+ --shadow-sm: 0 1px 3px 0 color-mix(in oklab, CanvasText 12%, transparent), 0 1px 2px -1px color-mix(in oklab, CanvasText 8%, transparent);
52
+ --shadow-md: 0 4px 6px -1px color-mix(in oklab, CanvasText 15%, transparent), 0 2px 4px -2px color-mix(in oklab, CanvasText 12%, transparent);
53
+ --shadow-lg: 0 10px 15px -3px color-mix(in oklab, CanvasText 18%, transparent), 0 4px 6px -4px color-mix(in oklab, CanvasText 15%, transparent);
54
+ --shadow-strong: 0 12px 30px color-mix(in oklab, CanvasText 25%, transparent);
55
+
56
+ /* Grid system - 8pt grid */
57
  --spacing-1: 4px;
58
  --spacing-2: 8px;
59
  --spacing-3: 12px;
 
64
  --spacing-10: 40px;
65
  --spacing-12: 48px;
66
 
67
+ /* Border radius - iOS style */
68
+ --radius-xs: 4px;
69
+ --radius-sm: 6px;
70
+ --radius-md: 8px;
71
+ --radius-lg: 10px;
72
  --radius-xl: 12px;
73
+ --radius-xxl: 16px;
74
  }
75
 
76
  @media (prefers-color-scheme: dark) {
 
118
  }
119
 
120
  body {
121
+ font-family: var(--font-sans);
122
  background: var(--bg);
123
  color: var(--text-primary);
124
  min-height: 100vh;
125
  min-height: 100svh;
126
  min-height: 100dvh;
127
  overflow: auto;
128
+ line-height: 1.6;
129
  -webkit-font-smoothing: antialiased;
130
  -moz-osx-font-smoothing: grayscale;
131
+ font-size: var(--size-body);
132
  letter-spacing: -0.01em;
133
  }
134
 
135
+ /* Typography Hierarchy - Apple HIG */
136
+ h1, h2, h3, h4, h5, h6 {
137
+ font-weight: 600;
138
+ letter-spacing: -0.01em;
139
+ margin: 0;
140
+ }
141
+
142
+ h1 { font-size: var(--size-title); }
143
+ h2 { font-size: var(--size-sub); }
144
+ h3 { font-size: var(--size-body); font-weight: 600; }
145
+
146
+ label, .meta {
147
+ font-size: var(--size-caption);
148
+ color: var(--text-tertiary);
149
+ font-weight: 500;
150
+ }
151
+
152
  /* Main App Container - Professional Layout */
153
  .app-container {
154
  display: grid;
 
935
  visibility: visible;
936
  }
937
 
938
+ /* Drawer container - Apple Materials */
939
  .drawer {
940
  position: fixed;
941
  top: 0;
 
945
  transform: translateX(-100%);
946
  transition: transform 0.35s ease;
947
  z-index: 1000;
948
+ backdrop-filter: blur(22px) saturate(180%);
949
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
950
+ background: color-mix(in oklab, Canvas 72%, CanvasText 8%);
951
+ border-right: 1px solid color-mix(in oklab, CanvasText 18%, transparent);
952
  box-shadow: var(--shadow-strong);
953
  overflow: hidden;
954
  display: flex;
 
1711
  .tab-btn:focus-visible {
1712
  outline: 2px solid var(--brand-primary);
1713
  outline-offset: -2px;
1714
+ }
1715
+
1716
+ /* ============================= */
1717
+ /* Apple HIG Enhanced Progress & Notifications */
1718
+ /* ============================= */
1719
+
1720
+ /* Enhanced Progress Ring for Generate Button */
1721
+ .generate-btn {
1722
+ position: relative;
1723
+ overflow: hidden;
1724
+ }
1725
+
1726
+ .generate-btn[aria-busy="true"] {
1727
+ pointer-events: none;
1728
+ opacity: 0.8;
1729
+ }
1730
+
1731
+ .generate-btn .progress-ring {
1732
+ position: absolute;
1733
+ inset: -4px;
1734
+ border-radius: 999px;
1735
+ border: 2px solid color-mix(in oklab, var(--brand-primary) 60%, transparent);
1736
+ border-top-color: transparent;
1737
+ animation: spin 0.8s linear infinite;
1738
+ }
1739
+
1740
+ @keyframes spin {
1741
+ to { transform: rotate(360deg); }
1742
+ }
1743
+
1744
+ /* Skeleton Loading States */
1745
+ .skeleton {
1746
+ background: linear-gradient(90deg, transparent, color-mix(in oklab, CanvasText 8%, transparent), transparent);
1747
+ background-size: 200% 100%;
1748
+ animation: shimmer 1.4s infinite;
1749
+ }
1750
+
1751
+ @keyframes shimmer {
1752
+ 0% { background-position: -100% 0; }
1753
+ 100% { background-position: 200% 0; }
1754
+ }
1755
+
1756
+ .preview-skeleton {
1757
+ width: 100%;
1758
+ height: 200px;
1759
+ border-radius: var(--radius-lg);
1760
+ background: var(--surface-secondary);
1761
+ }
1762
+
1763
+ /* Non-intrusive Banner System */
1764
+ .banner {
1765
+ position: fixed;
1766
+ top: max(var(--spacing-4), env(safe-area-inset-top));
1767
+ left: 50%;
1768
+ transform: translateX(-50%);
1769
+ z-index: 1100;
1770
+ backdrop-filter: blur(22px) saturate(180%);
1771
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
1772
+ background: color-mix(in oklab, Canvas 72%, CanvasText 8%);
1773
+ border: 1px solid color-mix(in oklab, CanvasText 18%, transparent);
1774
+ border-radius: var(--radius-xl);
1775
+ padding: var(--spacing-3) var(--spacing-5);
1776
+ box-shadow: var(--shadow-lg);
1777
+ max-width: 90vw;
1778
+ transition: all 0.3s ease;
1779
+ opacity: 0;
1780
+ visibility: hidden;
1781
+ transform: translateX(-50%) translateY(-20px);
1782
+ }
1783
+
1784
+ .banner.show {
1785
+ opacity: 1;
1786
+ visibility: visible;
1787
+ transform: translateX(-50%) translateY(0);
1788
+ }
1789
+
1790
+ .banner.success {
1791
+ border-color: var(--success);
1792
+ background: color-mix(in oklab, var(--success) 15%, Canvas 85%);
1793
+ }
1794
+
1795
+ .banner.error {
1796
+ border-color: var(--danger);
1797
+ background: color-mix(in oklab, var(--danger) 15%, Canvas 85%);
1798
+ }
1799
+
1800
+ .banner.warning {
1801
+ border-color: var(--warning);
1802
+ background: color-mix(in oklab, var(--warning) 15%, Canvas 85%);
1803
+ }
1804
+
1805
+ /* Toast System - Bottom Right */
1806
+ .toasts {
1807
+ position: fixed;
1808
+ bottom: max(var(--spacing-6), env(safe-area-inset-bottom));
1809
+ right: max(var(--spacing-6), env(safe-area-inset-right));
1810
+ z-index: 1100;
1811
+ display: flex;
1812
+ flex-direction: column;
1813
+ gap: var(--spacing-3);
1814
+ pointer-events: none;
1815
+ }
1816
+
1817
+ .toast {
1818
+ backdrop-filter: blur(22px) saturate(180%);
1819
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
1820
+ background: color-mix(in oklab, Canvas 82%, CanvasText 18%);
1821
+ border: 1px solid color-mix(in oklab, CanvasText 18%, transparent);
1822
+ border-radius: var(--radius-xl);
1823
+ padding: var(--spacing-4) var(--spacing-5);
1824
+ box-shadow: var(--shadow-lg);
1825
+ max-width: 350px;
1826
+ pointer-events: auto;
1827
+ transition: all 0.3s ease;
1828
+ opacity: 0;
1829
+ transform: translateX(100%);
1830
+ color: var(--text-primary);
1831
+ font-size: var(--size-caption);
1832
+ font-weight: 500;
1833
+ }
1834
+
1835
+ .toast.show {
1836
+ opacity: 1;
1837
+ transform: translateX(0);
1838
+ }
1839
+
1840
+ .toast.success {
1841
+ border-color: var(--success);
1842
+ background: color-mix(in oklab, var(--success) 12%, Canvas 88%);
1843
+ }
1844
+
1845
+ .toast.error {
1846
+ border-color: var(--danger);
1847
+ background: color-mix(in oklab, var(--danger) 12%, Canvas 88%);
1848
+ }
1849
+
1850
+ .toast.warning {
1851
+ border-color: var(--warning);
1852
+ background: color-mix(in oklab, var(--warning) 12%, Canvas 88%);
1853
+ }
1854
+
1855
+ .toast.info {
1856
+ border-color: var(--info);
1857
+ background: color-mix(in oklab, var(--info) 12%, Canvas 88%);
1858
+ }
1859
+
1860
+ .toast-header {
1861
+ display: flex;
1862
+ align-items: center;
1863
+ justify-content: space-between;
1864
+ margin-bottom: var(--spacing-2);
1865
+ font-weight: 600;
1866
+ }
1867
+
1868
+ .toast-close {
1869
+ background: none;
1870
+ border: none;
1871
+ color: var(--text-secondary);
1872
+ cursor: pointer;
1873
+ padding: var(--spacing-1);
1874
+ border-radius: var(--radius-sm);
1875
+ transition: all 0.2s ease;
1876
+ font-size: 16px;
1877
+ line-height: 1;
1878
+ }
1879
+
1880
+ .toast-close:hover {
1881
+ background: color-mix(in oklab, CanvasText 10%, transparent);
1882
+ color: var(--text-primary);
1883
+ }
1884
+
1885
+ /* Enhanced Progress Logs */
1886
+ .progress-logs {
1887
+ margin-top: var(--spacing-4);
1888
+ background: var(--surface-secondary);
1889
+ border: 1px solid var(--border-light);
1890
+ border-radius: var(--radius-lg);
1891
+ max-height: 120px;
1892
+ overflow-y: auto;
1893
+ -webkit-overflow-scrolling: touch;
1894
+ scrollbar-gutter: stable;
1895
+ display: none;
1896
+ font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
1897
+ font-size: 12px;
1898
+ }
1899
+
1900
+ .progress-logs.active {
1901
+ display: block;
1902
+ }
1903
+
1904
+ .progress-logs .log-entry {
1905
+ padding: var(--spacing-2) var(--spacing-3);
1906
+ color: var(--text-secondary);
1907
+ border-bottom: 1px solid var(--border-light);
1908
+ line-height: 1.4;
1909
+ }
1910
+
1911
+ .progress-logs .log-entry:last-child {
1912
+ border-bottom: none;
1913
+ }
1914
+
1915
+ .progress-logs .log-entry.success {
1916
+ color: var(--success);
1917
+ }
1918
+
1919
+ .progress-logs .log-entry.error {
1920
+ color: var(--danger);
1921
+ }
1922
+
1923
+ .progress-logs .log-entry.warning {
1924
+ color: var(--warning);
1925
+ }
1926
+
1927
+ /* Collapsed progress details */
1928
+ .progress-details-toggle {
1929
+ background: none;
1930
+ border: none;
1931
+ color: var(--brand-primary);
1932
+ cursor: pointer;
1933
+ font-size: var(--size-caption);
1934
+ padding: var(--spacing-2) 0;
1935
+ text-decoration: underline;
1936
+ transition: color 0.2s ease;
1937
+ }
1938
+
1939
+ .progress-details-toggle:hover {
1940
+ color: var(--brand-hover);
1941
+ }
1942
+
1943
+ /* Micro progress indicators for history */
1944
+ .generation-meta::before {
1945
+ content: "";
1946
+ width: 6px;
1947
+ height: 6px;
1948
+ border-radius: 50%;
1949
+ background: var(--success);
1950
+ display: inline-block;
1951
+ margin-right: var(--spacing-2);
1952
+ }
1953
+
1954
+ .generation-meta.processing::before {
1955
+ background: var(--warning);
1956
+ animation: pulse 1.5s ease-in-out infinite;
1957
+ }
1958
+
1959
+ .generation-meta.failed::before {
1960
+ background: var(--danger);
1961
+ }
1962
+
1963
+ @keyframes pulse {
1964
+ 0%, 100% { opacity: 0.6; }
1965
+ 50% { opacity: 1; }
1966
+ }
1967
+
1968
+ /* Toast action buttons */
1969
+ .toast-actions {
1970
+ margin-top: var(--spacing-3);
1971
+ display: flex;
1972
+ gap: var(--spacing-2);
1973
+ }
1974
+
1975
+ .toast-action {
1976
+ background: var(--brand-primary);
1977
+ color: white;
1978
+ border: none;
1979
+ padding: var(--spacing-2) var(--spacing-3);
1980
+ border-radius: var(--radius-md);
1981
+ font-size: var(--size-caption);
1982
+ font-weight: 500;
1983
+ cursor: pointer;
1984
+ transition: all 0.2s ease;
1985
+ }
1986
+
1987
+ .toast-action:hover {
1988
+ background: var(--brand-hover);
1989
+ transform: translateY(-1px);
1990
+ }
1991
+
1992
+ /* Enhanced button states */
1993
+ .generate-btn:not([aria-busy="true"]):hover {
1994
+ transform: translateY(-2px) scale(1.02);
1995
+ box-shadow: 0 8px 25px color-mix(in oklab, var(--brand-primary) 35%, transparent);
1996
+ }
1997
+
1998
+ .generate-btn:not([aria-busy="true"]):active {
1999
+ transform: translateY(0) scale(0.98);
2000
+ }
2001
+
2002
+ /* Form enhancements */
2003
+ .form-group input:hover:not(:focus),
2004
+ .form-group textarea:hover:not(:focus),
2005
+ .form-group select:hover:not(:focus) {
2006
+ border-color: var(--border-medium);
2007
+ background: var(--surface-secondary);
2008
+ }
2009
+
2010
+ /* Card hover enhancements */
2011
+ .card:hover {
2012
+ border-color: color-mix(in oklab, var(--brand-primary) 25%, transparent);
2013
+ box-shadow: var(--shadow-md);
2014
+ }
2015
+
2016
+ /* History item enhancements */
2017
+ .generation-item:hover {
2018
+ transform: translateY(-2px);
2019
+ box-shadow: var(--shadow-lg);
2020
+ }
2021
+
2022
+ /* API Key status enhancements */
2023
+ .api-key-status.testing::before {
2024
+ content: "⏳ ";
2025
+ }
2026
+
2027
+ .api-key-status.success::before {
2028
+ content: "✓ ";
2029
+ }
2030
+
2031
+ .api-key-status.error::before {
2032
+ content: "⚠ ";
2033
+ }
2034
+
2035
+ /* Reduced motion support */
2036
+ @media (prefers-reduced-motion: reduce) {
2037
+ .progress-ring,
2038
+ .skeleton,
2039
+ .generation-meta.processing::before {
2040
+ animation: none;
2041
+ }
2042
+
2043
+ .banner,
2044
+ .toast {
2045
+ transition: opacity 0.1s ease;
2046
+ }
2047
+
2048
+ .banner.show,
2049
+ .toast.show {
2050
+ transform: translateX(-50%) translateY(0);
2051
+ }
2052
+
2053
+ .generate-btn:hover,
2054
+ .generation-item:hover,
2055
+ .card:hover {
2056
+ transform: none;
2057
+ }
2058
  }
templates/index.html CHANGED
@@ -135,9 +135,10 @@
135
  </div>
136
  </div>
137
 
138
- <button id="generateBtn" class="generate-btn">
139
  <span class="btn-text">生成图像</span>
140
  <div class="spinner" style="display: none;"></div>
 
141
  </button>
142
 
143
  <div id="statusMessage" class="status-message"></div>
@@ -262,9 +263,10 @@
262
  </div>
263
  </div>
264
 
265
- <button id="generateBtn" class="generate-btn">
266
  <span class="btn-text">生成图像</span>
267
  <div class="spinner" style="display: none;"></div>
 
268
  </button>
269
 
270
  <div id="statusMessage" class="status-message"></div>
@@ -322,6 +324,10 @@
322
  </div>
323
  </div>
324
 
 
 
 
 
325
  <script src="/static/script.js"></script>
326
  </body>
327
  </html>
 
135
  </div>
136
  </div>
137
 
138
+ <button id="generateBtn" class="generate-btn" aria-busy="false">
139
  <span class="btn-text">生成图像</span>
140
  <div class="spinner" style="display: none;"></div>
141
+ <div class="progress-ring" style="display: none;"></div>
142
  </button>
143
 
144
  <div id="statusMessage" class="status-message"></div>
 
263
  </div>
264
  </div>
265
 
266
+ <button id="generateBtn" class="generate-btn" aria-busy="false">
267
  <span class="btn-text">生成图像</span>
268
  <div class="spinner" style="display: none;"></div>
269
+ <div class="progress-ring" style="display: none;"></div>
270
  </button>
271
 
272
  <div id="statusMessage" class="status-message"></div>
 
324
  </div>
325
  </div>
326
 
327
+ <!-- Non-intrusive Notification System -->
328
+ <div class="banner" role="status" aria-live="polite" hidden id="banner"></div>
329
+ <div class="toasts" aria-live="polite" aria-atomic="true" id="toasts"></div>
330
+
331
  <script src="/static/script.js"></script>
332
  </body>
333
  </html>