Skip to content

Commit dabf0e3

Browse files
committed
UI: Info toasts now blue, add colorless default level
Toasts of `info` type used to render colorless (grey left stripe), which didn't match the warn/error pattern users expect. Now `info` carries a blue stripe + faint tinted bg, and a new `default` level takes over the colorless slot for low-importance feedback (tab limits, copy/cut confirmations, in-progress hints) where a color flash would be noisy. - New `--color-toast-info-stripe` / `--color-toast-info-bg` tokens in light, dark, dark-old-WebKit, and force-old-WebKit paths. - `default` is the new fallback level. Existing call sites without an explicit level keep their colorless treatment. - Audited every `addToast` call site: NetworkBrowser/ShareBrowser action-confirms moved to `success`; MTP F5/F6 instructional toasts and the hidden-file rename explanation moved to `info`; frequent clipboard confirmations and tab/recent-tabs limits stay `default`. - `apps/desktop/src/lib/ui/CLAUDE.md` documents all five levels with examples, a tiebreaker rule, and a "common mistakes" list so future agents pick the right one. - `DebugToastPanel` gets Default + Info buttons in both Transient and Persistent rows for visual previews.
1 parent 39fc8d2 commit dabf0e3

11 files changed

Lines changed: 77 additions & 22 deletions

File tree

apps/desktop/src/app.css

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@
135135
--color-disk-danger: var(--color-error);
136136
--color-disk-track: var(--color-border);
137137

138-
/* === Toast tints (tuned per scheme so warn reads amber, error reads red) === */
138+
/* === Toast tints (tuned per scheme so info reads blue, warn reads amber, error reads red) === */
139+
--color-toast-info-stripe: #2563eb;
140+
--color-toast-info-bg: #eff6ff;
139141
--color-toast-warn-stripe: #d4a006;
140142
--color-toast-warn-bg: #fef9ee;
141143
--color-toast-error-bg: #fef2f2;
@@ -570,6 +572,8 @@
570572
--color-disk-track: var(--color-border);
571573

572574
/* === Toast tints === */
575+
--color-toast-info-stripe: #60a5fa;
576+
--color-toast-info-bg: color-mix(in srgb, #60a5fa 8%, var(--color-bg-secondary));
573577
--color-toast-warn-stripe: var(--color-warning);
574578
--color-toast-warn-bg: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-secondary));
575579
--color-toast-error-bg: color-mix(in srgb, var(--color-error) 8%, var(--color-bg-secondary));
@@ -665,6 +669,7 @@
665669
--color-warning-bg-solid: #483d29;
666670
--color-disk-ok: #588b5b;
667671
--color-disk-warning: #bf892d;
672+
--color-toast-info-bg: #1f2937;
668673
--color-toast-warn-bg: #3a3429;
669674
--color-toast-error-bg: #3a2c2b;
670675
--color-toast-success-bg: #29362e;
@@ -693,6 +698,7 @@
693698
--color-warning-bg-solid: #483d29;
694699
--color-disk-ok: #588b5b;
695700
--color-disk-warning: #bf892d;
701+
--color-toast-info-bg: #1f2937;
696702
--color-toast-warn-bg: #3a3429;
697703
--color-toast-error-bg: #3a2c2b;
698704
--color-toast-success-bg: #29362e;

apps/desktop/src/lib/file-explorer/network/NetworkBrowser.svelte

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@
461461
/** Remove a manual host after confirmation. For discovered hosts, show a toast. */
462462
async function handleRemoveHost(host: NetworkHost) {
463463
if (host.source !== 'manual') {
464-
addToast(`Can't remove discovered hosts`, { level: 'info' })
464+
addToast(`Can't remove discovered hosts`)
465465
return
466466
}
467467
@@ -470,7 +470,7 @@
470470
471471
try {
472472
await removeManualServer(host.id)
473-
addToast(`Removed ${host.name}`, { level: 'info' })
473+
addToast(`Removed ${host.name}`, { level: 'success' })
474474
} catch {
475475
addToast(`Couldn't remove ${host.name}`, { level: 'error' })
476476
}
@@ -503,7 +503,7 @@
503503
case 'forget-password': {
504504
try {
505505
await forgetCredentials(payload.hostName)
506-
addToast(`Forgot saved password for ${payload.hostName}`, { level: 'info' })
506+
addToast(`Forgot saved password for ${payload.hostName}`, { level: 'success' })
507507
} catch {
508508
addToast(`Couldn't delete saved password`, { level: 'error' })
509509
}
@@ -515,9 +515,9 @@
515515
try {
516516
const unmounted = await disconnectNetworkHost(host.id, host.name, host.ipAddress)
517517
if (unmounted.length > 0) {
518-
addToast(`Disconnected from ${payload.hostName}`, { level: 'info' })
518+
addToast(`Disconnected from ${payload.hostName}`, { level: 'success' })
519519
} else {
520-
addToast(`No mounted shares from ${payload.hostName}`, { level: 'info' })
520+
addToast(`No mounted shares from ${payload.hostName}`)
521521
}
522522
} catch (e) {
523523
addToast(`Couldn't disconnect: ${String(e)}`, { level: 'error' })

apps/desktop/src/lib/file-explorer/network/ShareBrowser.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@
443443
try {
444444
await forgetCredentials(host.name)
445445
authenticatedCredentials = null
446-
addToast(`Forgot saved password for ${host.name}`, { level: 'info' })
446+
addToast(`Forgot saved password for ${host.name}`, { level: 'success' })
447447
} catch {
448448
addToast(`Couldn't delete saved password`, { level: 'error' })
449449
}

apps/desktop/src/lib/file-explorer/pane/DualPaneExplorer.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,7 +1582,7 @@
15821582
if (!state) return
15831583
15841584
if (state.volumeId.startsWith('mtp-')) {
1585-
addToast('Use F5 to copy files from MTP devices')
1585+
addToast('Use F5 to copy files from MTP devices', { level: 'info' })
15861586
return
15871587
}
15881588
@@ -1606,7 +1606,7 @@
16061606
if (!state) return
16071607
16081608
if (state.volumeId.startsWith('mtp-')) {
1609-
addToast('Use F6 to move files from MTP devices')
1609+
addToast('Use F6 to move files from MTP devices', { level: 'info' })
16101610
return
16111611
}
16121612
@@ -1631,7 +1631,7 @@
16311631
// no point reading the system clipboard just to reject it.
16321632
const volumeId = getPaneVolumeId(focusedPane)
16331633
if (volumeId.startsWith('mtp-')) {
1634-
addToast('Use F5 to copy files to MTP devices')
1634+
addToast('Use F5 to copy files to MTP devices', { level: 'info' })
16351635
return
16361636
}
16371637

apps/desktop/src/lib/file-explorer/pane/rename-flow.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function createRenameFlow(deps: RenameFlowDeps) {
110110
pendingCursorName = newName
111111

112112
if (wasHiddenRename) {
113-
addToast("Your file disappeared from view because hidden files aren't shown.")
113+
addToast("Your file disappeared from view because hidden files aren't shown.", { level: 'info' })
114114
}
115115
}
116116

apps/desktop/src/lib/ui/CLAUDE.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,33 @@ Centralized toast notifications with stacking, levels, and two dismissal modes.
175175
- **Container** (`ToastContainer.svelte`): Mounted once in `(main)/+layout.svelte`. Fixed top-right, stacks vertically.
176176
- **Item** (`ToastItem.svelte`): Frame, close button, auto-dismiss timer for transient toasts.
177177

178-
Levels: `info` (default), `success`, `warn`, `error`. Dismissal: `transient` (4s timeout + nav-dismiss, default) or
179-
`persistent`.
178+
Five levels. Pick by what kind of feedback the toast carries, not by how the message reads:
179+
180+
- **`default`** (no color, the fallback): low-importance feedback that doesn't warrant a color flash. Tab/clipboard
181+
limits, "nothing to do" messages (`No recently closed tabs`), copy/cut confirmations (`Copied N items`), persistent
182+
in-progress indicators (`Connecting directly…`), repeated educational hints (Quick Look Space-press hint), soft
183+
refusals (`Can't remove discovered hosts`).
184+
- **`info`** (blue): true notices the user should attend to. Restart hints (`Restart Cmdr to apply…`), instructional
185+
cues triggered by a wrong move (`Use F5 to copy files from MTP devices`), soft explanations of unexpected UI state
186+
(`Your file disappeared from view because hidden files aren't shown.`), background activity the user opted into
187+
(`Error report sent`).
188+
- **`success`** (green): one-shot confirmations of user-initiated actions that aren't routine. Host removed, share
189+
disconnected, password forgotten, direct SMB upgrade succeeded. NOT for frequent actions like clipboard copy.
190+
- **`warn`** (amber): something the user tried isn't quite working, but no operation failed and no data is at risk.
191+
Examples: `Share 'X' not found on Y`, rename-conflict notices that don't abort the rename.
192+
- **`error`** (red): an attempted operation actually failed. Examples: `Couldn't remove ${host}`,
193+
`Direct connection failed: …`, `Couldn't delete saved password`. Inline "Send error report…" button auto-attaches for
194+
string-content errors.
195+
196+
Tiebreaker: when unsure between two adjacent levels, pick the lower-intensity one. Frequent feedback should be quiet;
197+
the user can read the text. Color is for the few cases where attention is warranted.
198+
199+
Common mistakes to avoid: don't pick `success` for every action confirmation (frequent ones like "copied N items" stay
200+
`default`); don't pick `info` for in-progress spinners or repeating hints (those are `default`); don't pick `error` for
201+
soft refusals like "tab limit reached" (that's `default`); don't pick `warn` when an op actually failed (that's
202+
`error`).
203+
204+
Dismissal: `transient` (4s timeout + nav-dismiss, default) or `persistent`.
180205

181206
Call `dismissTransientToasts()` on pane navigation to clear stale feedback.
182207

apps/desktop/src/lib/ui/toast/ToastContainer.a11y.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ describe('ToastContainer a11y', () => {
2424
await expectNoA11yViolations(target)
2525
})
2626

27-
it('with a single info toast has no a11y violations', async () => {
28-
addToast('Your file has been copied', { level: 'info' })
27+
it('with a single default toast has no a11y violations', async () => {
28+
addToast('Your file has been copied')
2929
const target = document.createElement('div')
3030
document.body.appendChild(target)
3131
mount(ToastContainer, { target, props: {} })
@@ -34,6 +34,7 @@ describe('ToastContainer a11y', () => {
3434
})
3535

3636
it('with mixed toast levels has no a11y violations', async () => {
37+
addToast('Restart Cmdr to apply', { level: 'info' })
3738
addToast('Saved', { level: 'success' })
3839
addToast('Watch out: slow mount', { level: 'warn' })
3940
addToast('Connection lost', { level: 'error' })

apps/desktop/src/lib/ui/toast/ToastItem.svelte

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,11 @@
5151

5252
<div
5353
class="toast"
54+
class:info={level === 'info'}
5455
class:success={level === 'success'}
5556
class:warn={level === 'warn'}
5657
class:error={level === 'error'}
57-
role={level === 'info' || level === 'success' ? 'status' : 'alert'}
58+
role={level === 'default' || level === 'info' || level === 'success' ? 'status' : 'alert'}
5859
>
5960
<div class="toast-content">
6061
{#if typeof content === 'string'}
@@ -106,6 +107,11 @@
106107
gap: var(--spacing-sm);
107108
}
108109
110+
.toast.info {
111+
border-left-color: var(--color-toast-info-stripe);
112+
background: var(--color-toast-info-bg);
113+
}
114+
109115
.toast.success {
110116
border-left-color: var(--color-toast-success-stripe);
111117
background: var(--color-toast-success-bg);

apps/desktop/src/lib/ui/toast/toast-store.svelte.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Component } from 'svelte'
22

3-
export type ToastLevel = 'info' | 'success' | 'warn' | 'error'
3+
export type ToastLevel = 'default' | 'info' | 'success' | 'warn' | 'error'
44
export type ToastDismissal = 'transient' | 'persistent'
55

66
/** Content can be a plain string (rendered as text) or a Svelte component (mounted as-is). */
@@ -75,7 +75,7 @@ function makeRoomForNewToast(): boolean {
7575
}
7676

7777
export function addToast(content: ToastContent, options?: ToastOptions): string {
78-
const level = options?.level ?? 'info'
78+
const level = options?.level ?? 'default'
7979
const dismissal = options?.dismissal ?? 'transient'
8080
const timeoutMs = dismissal === 'persistent' ? 0 : (options?.timeoutMs ?? 4000)
8181
const id = options?.id ?? crypto.randomUUID()

apps/desktop/src/lib/ui/toast/toast-store.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ describe('closeTooltip and onDismiss', () => {
127127
})
128128

129129
describe('default values', () => {
130-
it('defaults level to info', () => {
130+
it('defaults level to default', () => {
131131
addToast(dummyContent)
132-
expect(getToasts()[0].level).toBe('info')
132+
expect(getToasts()[0].level).toBe('default')
133133
})
134134

135135
it('defaults dismissal to transient', () => {

0 commit comments

Comments
 (0)