Skip to content

Commit 2329f2f

Browse files
committed
Use new toasts for existing features
- Centralized store, container, and item in src/lib/ui/toast/ - Two dismissal modes (transient with 4s timeout, persistent) - Three levels (info, warn, error), content as string or Component - Dedup via optional ID, max 5 visible with stacking - Migrated FilePane rename, Update, and AI notifications - Deleted Notification.svelte, UpdateNotification.svelte, AiNotification.svelte - Added transfer success toast in DualPaneExplorer - Added toast debug buttons in Debug window (Cmd+D)
1 parent 9b3f963 commit 2329f2f

19 files changed

Lines changed: 469 additions & 343 deletions

apps/desktop/coverage-allowlist.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"ui/AlertDialog.svelte": { "reason": "Simple UI modal for informational messages" },
66
"ui/dialog-registry.ts": { "reason": "Pure constant and type definition, no logic to test" },
77
"ui/ModalDialog.svelte": { "reason": "Shared modal dialog wrapper, depends on DOM APIs and Tauri commands" },
8-
"updates/UpdateNotification.svelte": { "reason": "UI component, needs component testing" },
8+
"updates/UpdateToastContent.svelte": { "reason": "UI component, toast content for update prompt" },
99
"app-status-store.ts": { "reason": "Depends on Tauri APIs" },
1010
"benchmark.ts": { "reason": "Dev tooling, not critical path" },
1111
"file-explorer/drag/drag-drop.ts": { "reason": "Needs integration testing with Tauri" },

apps/desktop/src/app.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
--color-warning: #e65100;
5050
--color-warning-bg: rgba(230, 81, 0, 0.1);
5151

52+
/* === Toast tints (tuned per scheme so warn reads amber, error reads red) === */
53+
--color-toast-warn-stripe: #d4a006;
54+
--color-toast-warn-bg: #fef9ee;
55+
--color-toast-error-bg: #fef2f2;
56+
5257
/* === Selection === */
5358
--color-selection-fg: #c9a227;
5459

@@ -152,6 +157,11 @@
152157
--color-warning: #f5a623;
153158
--color-warning-bg: rgba(245, 166, 35, 0.15);
154159

160+
/* === Toast tints === */
161+
--color-toast-warn-stripe: var(--color-warning);
162+
--color-toast-warn-bg: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-secondary));
163+
--color-toast-error-bg: color-mix(in srgb, var(--color-error) 8%, var(--color-bg-secondary));
164+
155165
/* === Selection === */
156166
--color-selection-fg: #d4a82a;
157167

apps/desktop/src/lib/ai/AiNotification.svelte

Lines changed: 0 additions & 193 deletions
This file was deleted.

apps/desktop/src/lib/ai/AiNotification.test.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest'
22
import { mount, flushSync } from 'svelte'
3-
import AiNotification from './AiNotification.svelte'
43

54
// Track the state object so tests can mutate it
65
let mockState = {
@@ -17,17 +16,22 @@ let mockState = {
1716

1817
vi.mock('./ai-state.svelte', () => ({
1918
getAiState: () => mockState,
20-
initAiState: vi.fn(() => Promise.resolve(() => {})),
2119
handleDownload: vi.fn(() => Promise.resolve()),
2220
handleCancel: vi.fn(() => Promise.resolve()),
2321
handleDismiss: vi.fn(() => Promise.resolve()),
2422
handleOptOut: vi.fn(() => Promise.resolve()),
2523
handleGotIt: vi.fn(),
2624
}))
2725

26+
vi.mock('$lib/ui/toast', () => ({
27+
addToast: vi.fn(),
28+
dismissToast: vi.fn(),
29+
}))
30+
31+
import AiToastContent from './AiToastContent.svelte'
2832
import { handleDownload, handleCancel, handleDismiss, handleOptOut, handleGotIt } from './ai-state.svelte'
2933

30-
describe('AiNotification', () => {
34+
describe('AiToastContent', () => {
3135
beforeEach(() => {
3236
vi.clearAllMocks()
3337
mockState = {
@@ -46,7 +50,7 @@ describe('AiNotification', () => {
4650
it('renders offer notification with download size and settings hint', () => {
4751
mockState.notificationState = 'offer'
4852
const target = document.createElement('div')
49-
mount(AiNotification, { target })
53+
mount(AiToastContent, { target })
5054

5155
const description = target.querySelector('.ai-description')
5256
expect(description?.textContent).toContain('2.1 GB')
@@ -57,17 +61,14 @@ describe('AiNotification', () => {
5761

5862
it('renders nothing when state is hidden', () => {
5963
const target = document.createElement('div')
60-
mount(AiNotification, { target })
61-
expect(target.querySelector('.ai-notification')).toBeNull()
64+
mount(AiToastContent, { target })
65+
expect(target.querySelector('.ai-content')).toBeNull()
6266
})
6367

6468
it("renders offer notification with Download, Not now, and I don't want AI buttons", () => {
6569
mockState.notificationState = 'offer'
6670
const target = document.createElement('div')
67-
mount(AiNotification, { target })
68-
69-
const notification = target.querySelector('.ai-notification')
70-
expect(notification).not.toBeNull()
71+
mount(AiToastContent, { target })
7172

7273
const title = target.querySelector('.ai-title')
7374
expect(title?.textContent).toBe('AI features available')
@@ -82,7 +83,7 @@ describe('AiNotification', () => {
8283
it('calls handleDownload when Download is clicked', () => {
8384
mockState.notificationState = 'offer'
8485
const target = document.createElement('div')
85-
mount(AiNotification, { target })
86+
mount(AiToastContent, { target })
8687

8788
const downloadButton = target.querySelector('.ai-button.primary') as HTMLButtonElement
8889
downloadButton.click()
@@ -94,7 +95,7 @@ describe('AiNotification', () => {
9495
it('calls handleDismiss when Not now is clicked', () => {
9596
mockState.notificationState = 'offer'
9697
const target = document.createElement('div')
97-
mount(AiNotification, { target })
98+
mount(AiToastContent, { target })
9899

99100
const dismissButton = target.querySelector('.ai-button.secondary') as HTMLButtonElement
100101
dismissButton.click()
@@ -106,7 +107,7 @@ describe('AiNotification', () => {
106107
it("calls handleOptOut when I don't want AI is clicked", () => {
107108
mockState.notificationState = 'offer'
108109
const target = document.createElement('div')
109-
mount(AiNotification, { target })
110+
mount(AiToastContent, { target })
110111

111112
const optOutButton = target.querySelector('.ai-button.tertiary') as HTMLButtonElement
112113
optOutButton.click()
@@ -121,7 +122,7 @@ describe('AiNotification', () => {
121122
mockState.progressText = '12% — 500.0 KB / 4.0 MB — 100.0 KB/s — 35s remaining'
122123

123124
const target = document.createElement('div')
124-
mount(AiNotification, { target })
125+
mount(AiToastContent, { target })
125126

126127
const title = target.querySelector('.ai-title')
127128
expect(title?.textContent).toBe('Downloading AI model...')
@@ -135,7 +136,7 @@ describe('AiNotification', () => {
135136
mockState.downloadProgress = null
136137

137138
const target = document.createElement('div')
138-
mount(AiNotification, { target })
139+
mount(AiToastContent, { target })
139140

140141
const progressText = target.querySelector('.ai-progress-text')
141142
expect(progressText?.textContent).toBe('Starting download...')
@@ -146,7 +147,7 @@ describe('AiNotification', () => {
146147
mockState.downloadProgress = { bytesDownloaded: 100, totalBytes: 1000, speed: 50, etaSeconds: 18 }
147148

148149
const target = document.createElement('div')
149-
mount(AiNotification, { target })
150+
mount(AiToastContent, { target })
150151

151152
const cancelButton = target.querySelector('.ai-button.secondary') as HTMLButtonElement
152153
cancelButton.click()
@@ -159,7 +160,7 @@ describe('AiNotification', () => {
159160
mockState.notificationState = 'installing'
160161

161162
const target = document.createElement('div')
162-
mount(AiNotification, { target })
163+
mount(AiToastContent, { target })
163164

164165
const title = target.querySelector('.ai-title')
165166
expect(title?.textContent).toBe('Setting up AI...')
@@ -175,7 +176,7 @@ describe('AiNotification', () => {
175176
mockState.notificationState = 'ready'
176177

177178
const target = document.createElement('div')
178-
mount(AiNotification, { target })
179+
mount(AiToastContent, { target })
179180

180181
const title = target.querySelector('.ai-title')
181182
expect(title?.textContent).toBe('AI ready')
@@ -188,7 +189,7 @@ describe('AiNotification', () => {
188189
mockState.notificationState = 'ready'
189190

190191
const target = document.createElement('div')
191-
mount(AiNotification, { target })
192+
mount(AiToastContent, { target })
192193

193194
const button = target.querySelector('.ai-button.primary') as HTMLButtonElement
194195
button.click()
@@ -203,7 +204,7 @@ describe('AiNotification', () => {
203204
mockState.progressText = '50% — 2.0 MB / 4.0 MB'
204205

205206
const target = document.createElement('div')
206-
mount(AiNotification, { target })
207+
mount(AiToastContent, { target })
207208

208209
const progressBar = target.querySelector('.progress-bar-fill') as HTMLElement
209210
expect(progressBar).not.toBeNull()

0 commit comments

Comments
 (0)