Skip to content

Commit 8dc2e33

Browse files
committed
Unify button styles and enforce macOS-native cursor
- Migrate 6 dialog/toast files to use Button.svelte instead of local primary/secondary/danger styles - Remove cursor: pointer globally (ress-reset, Button, and 16 components) — macOS apps use default arrow - Fix deviations: filter:brightness hover → accent-hover, disabled opacity 0.5 → 0.4, missing font-weight: 500, no-op hovers, narrow transitions - Update design system doc to match
1 parent ff0c27e commit 8dc2e33

29 files changed

Lines changed: 157 additions & 410 deletions

.interface-design/system.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ font-size: var(--font-size-sm); /* 12px */
413413
border-radius: var(--radius-sm); /* 4px */
414414
```
415415

416-
**All buttons:** `cursor: pointer`. No `outline: none` on focus — rely on focus-visible ring.
416+
**All buttons:** `cursor: default`. No `outline: none` on focus — rely on focus-visible ring.
417417

418418
### Buttons (website)
419419

apps/desktop/src/app.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@
247247

248248
a {
249249
background-color: transparent;
250+
cursor: default;
250251
}
251252

252253
a:active,
@@ -330,7 +331,7 @@
330331
[type='reset'],
331332
[type='submit'],
332333
button {
333-
cursor: pointer;
334+
cursor: default;
334335
}
335336

336337
[type='reset'],

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe('AiToastContent', () => {
7373
const title = target.querySelector('.ai-title')
7474
expect(title?.textContent).toBe('AI features available')
7575

76-
const buttons = target.querySelectorAll('.ai-button')
76+
const buttons = target.querySelectorAll('.ai-actions button')
7777
expect(buttons).toHaveLength(3)
7878
expect(buttons[0].textContent).toBe('Download')
7979
expect(buttons[1].textContent).toBe('Not now')
@@ -85,7 +85,7 @@ describe('AiToastContent', () => {
8585
const target = document.createElement('div')
8686
mount(AiToastContent, { target })
8787

88-
const downloadButton = target.querySelector('.ai-button.primary') as HTMLButtonElement
88+
const downloadButton = target.querySelector('.btn-primary') as HTMLButtonElement
8989
downloadButton.click()
9090
flushSync()
9191

@@ -97,7 +97,7 @@ describe('AiToastContent', () => {
9797
const target = document.createElement('div')
9898
mount(AiToastContent, { target })
9999

100-
const dismissButton = target.querySelector('.ai-button.secondary') as HTMLButtonElement
100+
const dismissButton = target.querySelector('.btn-secondary') as HTMLButtonElement
101101
dismissButton.click()
102102
flushSync()
103103

@@ -109,7 +109,7 @@ describe('AiToastContent', () => {
109109
const target = document.createElement('div')
110110
mount(AiToastContent, { target })
111111

112-
const optOutButton = target.querySelector('.ai-button.tertiary') as HTMLButtonElement
112+
const optOutButton = target.querySelector('.tertiary-link') as HTMLButtonElement
113113
optOutButton.click()
114114
flushSync()
115115

@@ -149,7 +149,7 @@ describe('AiToastContent', () => {
149149
const target = document.createElement('div')
150150
mount(AiToastContent, { target })
151151

152-
const cancelButton = target.querySelector('.ai-button.secondary') as HTMLButtonElement
152+
const cancelButton = target.querySelector('.btn-secondary') as HTMLButtonElement
153153
cancelButton.click()
154154
flushSync()
155155

@@ -169,7 +169,7 @@ describe('AiToastContent', () => {
169169
expect(description?.textContent).toBe('Starting inference server')
170170

171171
// No buttons in installing state
172-
expect(target.querySelectorAll('.ai-button')).toHaveLength(0)
172+
expect(target.querySelectorAll('.ai-actions button')).toHaveLength(0)
173173
})
174174

175175
it('renders ready state with Got it button', () => {
@@ -181,7 +181,7 @@ describe('AiToastContent', () => {
181181
const title = target.querySelector('.ai-title')
182182
expect(title?.textContent).toBe('AI ready')
183183

184-
const button = target.querySelector('.ai-button.primary') as HTMLButtonElement
184+
const button = target.querySelector('.btn-primary') as HTMLButtonElement
185185
expect(button.textContent).toBe('Got it')
186186
})
187187

@@ -191,7 +191,7 @@ describe('AiToastContent', () => {
191191
const target = document.createElement('div')
192192
mount(AiToastContent, { target })
193193

194-
const button = target.querySelector('.ai-button.primary') as HTMLButtonElement
194+
const button = target.querySelector('.btn-primary') as HTMLButtonElement
195195
button.click()
196196
flushSync()
197197

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

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
handleGotIt,
88
handleOptOut,
99
} from './ai-state.svelte'
10+
import Button from '$lib/ui/Button.svelte'
1011
1112
const aiState = getAiState()
1213
</script>
@@ -20,9 +21,9 @@
2021
<span class="ai-hint">You can add or remove AI later in settings.</span>
2122
</div>
2223
<div class="ai-actions">
23-
<button class="ai-button primary" onclick={() => void handleDownload()}>Download</button>
24-
<button class="ai-button secondary" onclick={() => void handleDismiss()}>Not now</button>
25-
<button class="ai-button tertiary" onclick={() => void handleOptOut()}>I don't want AI</button>
24+
<Button variant="primary" size="mini" onclick={() => void handleDownload()}>Download</Button>
25+
<Button variant="secondary" size="mini" onclick={() => void handleDismiss()}>Not now</Button>
26+
<button class="tertiary-link" onclick={() => void handleOptOut()}>I don't want AI</button>
2627
</div>
2728
{:else if aiState.notificationState === 'downloading'}
2829
<div class="ai-content">
@@ -44,7 +45,7 @@
4445
{/if}
4546
</div>
4647
<div class="ai-actions">
47-
<button class="ai-button secondary" onclick={() => void handleCancel()}>Cancel</button>
48+
<Button variant="secondary" size="mini" onclick={() => void handleCancel()}>Cancel</Button>
4849
</div>
4950
{:else if aiState.notificationState === 'installing'}
5051
<div class="ai-content">
@@ -57,7 +58,7 @@
5758
<span class="ai-description">Try creating a new folder (F7) to see AI-powered name suggestions.</span>
5859
</div>
5960
<div class="ai-actions">
60-
<button class="ai-button primary" onclick={handleGotIt}>Got it</button>
61+
<Button variant="primary" size="mini" onclick={handleGotIt}>Got it</Button>
6162
</div>
6263
{:else if aiState.notificationState === 'starting'}
6364
<div class="ai-content">
@@ -120,39 +121,21 @@
120121
margin-top: var(--spacing-xs);
121122
}
122123
123-
.ai-button {
124-
padding: var(--spacing-xs) var(--spacing-sm);
125-
border-radius: var(--radius-md);
126-
font-size: var(--font-size-sm);
127-
cursor: pointer;
128-
border: none;
129-
}
130-
131-
.ai-button.primary {
132-
background: var(--color-accent);
133-
color: #fff;
134-
}
135-
136-
.ai-button.primary:hover {
137-
filter: brightness(1.1);
138-
}
139-
140-
.ai-button.secondary {
141-
background: transparent;
142-
color: var(--color-text-secondary);
143-
}
144-
145-
.ai-button.secondary:hover {
146-
background: var(--color-bg-tertiary);
147-
}
148-
149-
.ai-button.tertiary {
124+
.tertiary-link {
150125
background: transparent;
151126
color: var(--color-text-tertiary);
152127
font-size: var(--font-size-xs);
128+
border: none;
129+
padding: var(--spacing-xs) var(--spacing-sm);
130+
transition: all var(--transition-base);
153131
}
154132
155-
.ai-button.tertiary:hover {
133+
.tertiary-link:hover {
156134
color: var(--color-text-secondary);
157135
}
136+
137+
.tertiary-link:focus-visible {
138+
outline: 2px solid var(--color-accent);
139+
outline-offset: 1px;
140+
}
158141
</style>

apps/desktop/src/lib/command-palette/CommandPalette.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@
252252
justify-content: space-between;
253253
align-items: center;
254254
padding: var(--spacing-sm) var(--spacing-lg);
255-
cursor: pointer;
256255
font-size: var(--font-size-md);
257256
color: var(--color-text-primary);
258257
}

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

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Uses the shared network-store for host data (initialized at app startup).
66
*/
77
import { onMount } from 'svelte'
8+
import Button from '$lib/ui/Button.svelte'
89
import {
910
getNetworkHosts,
1011
getDiscoveryState,
@@ -399,7 +400,7 @@
399400
</div>
400401

401402
<div class="refresh-section">
402-
<button type="button" class="refresh-button" onclick={handleRefreshClick}> 🔄 Refresh </button>
403+
<Button variant="secondary" onclick={handleRefreshClick}>🔄 Refresh</Button>
403404
</div>
404405
</div>
405406

@@ -525,26 +526,4 @@
525526
padding: var(--spacing-lg) var(--spacing-sm);
526527
border-top: 1px solid var(--color-border-subtle);
527528
}
528-
529-
.refresh-button {
530-
display: flex;
531-
align-items: center;
532-
gap: var(--spacing-sm);
533-
padding: var(--spacing-sm) var(--spacing-lg);
534-
border: 1px solid var(--color-border-strong);
535-
border-radius: var(--radius-md);
536-
background-color: var(--color-bg-secondary);
537-
color: var(--color-text-primary);
538-
font-size: var(--font-size-sm);
539-
cursor: pointer;
540-
transition: background-color var(--transition-base);
541-
}
542-
543-
.refresh-button:hover {
544-
background-color: var(--color-bg-tertiary);
545-
}
546-
547-
.refresh-button:active {
548-
background-color: var(--color-bg-tertiary);
549-
}
550529
</style>

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

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Displayed in the file pane when credentials are required to connect to a network share.
55
*/
66
import { onMount } from 'svelte'
7+
import Button from '$lib/ui/Button.svelte'
78
import type { AuthMode, ConnectionMode, KnownNetworkShare, NetworkHost } from '../types'
89
import { getUsernameHints, getKnownShareByName } from '$lib/tauri-commands'
910
@@ -195,17 +196,17 @@
195196
</div>
196197

197198
<div class="button-row">
198-
<button type="button" class="btn btn-secondary" onclick={onCancel} disabled={isConnecting}>
199-
Cancel
200-
</button>
201-
<button type="submit" class="btn btn-primary" disabled={!canSubmit || isConnecting}>
202-
{#if isConnecting}
203-
<span class="spinner"></span>
204-
Connecting...
205-
{:else}
206-
Connect
207-
{/if}
208-
</button>
199+
<Button variant="secondary" onclick={onCancel} disabled={isConnecting}>Cancel</Button>
200+
<Button variant="primary" type="submit" disabled={!canSubmit || isConnecting}>
201+
<span class="btn-content">
202+
{#if isConnecting}
203+
<span class="spinner"></span>
204+
Connecting...
205+
{:else}
206+
Connect
207+
{/if}
208+
</span>
209+
</Button>
209210
</div>
210211
</form>
211212
</div>
@@ -381,43 +382,10 @@
381382
margin-top: 20px;
382383
}
383384
384-
.btn {
385-
padding: 10px 20px;
386-
border-radius: var(--radius-md);
387-
font-size: var(--font-size-sm);
388-
font-weight: 500;
389-
cursor: pointer;
390-
transition:
391-
background-color var(--transition-base),
392-
opacity var(--transition-base);
393-
}
394-
395-
.btn:disabled {
396-
opacity: 0.5;
397-
cursor: not-allowed;
398-
}
399-
400-
.btn-secondary {
401-
background-color: var(--color-bg-tertiary);
402-
border: 1px solid var(--color-border-strong);
403-
color: var(--color-text-primary);
404-
}
405-
406-
.btn-secondary:hover:not(:disabled) {
407-
background-color: var(--color-bg-tertiary);
408-
}
409-
410-
.btn-primary {
385+
.btn-content {
411386
display: flex;
412387
align-items: center;
413388
gap: 8px;
414-
background-color: var(--color-accent);
415-
border: none;
416-
color: white;
417-
}
418-
419-
.btn-primary:hover:not(:disabled) {
420-
filter: brightness(1.1);
421389
}
422390
423391
.spinner {

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

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Shows login form when authentication is required.
55
*/
66
import { onMount } from 'svelte'
7+
import Button from '$lib/ui/Button.svelte'
78
import type { AuthMode, NetworkHost, ShareInfo, ShareListError } from '../types'
89
import {
910
getShareState,
@@ -394,21 +395,21 @@
394395
<div class="error-title">Couldn't connect to {host.name}</div>
395396
<div class="error-message">{error.message || error.type}</div>
396397
<div class="error-actions">
397-
<button type="button" class="btn" onclick={handleRetry}>Retry</button>
398-
<button type="button" class="btn" onclick={() => (showLoginForm = true)}>Sign in</button>
399-
<button type="button" class="btn" onclick={onBack}>Back</button>
398+
<Button variant="secondary" onclick={handleRetry}>Retry</Button>
399+
<Button variant="secondary" onclick={() => (showLoginForm = true)}>Sign in</Button>
400+
<Button variant="secondary" onclick={onBack}>Back</Button>
400401
</div>
401402
</div>
402403
{:else if sortedShares.length === 0}
403404
<div class="empty-state">
404405
<div class="empty-icon">📁</div>
405406
<div class="empty-title">No shares available</div>
406407
<div class="empty-message">This host has no accessible shares.</div>
407-
<button type="button" class="btn" onclick={onBack}>Back</button>
408+
<Button variant="secondary" onclick={onBack}>Back</Button>
408409
</div>
409410
{:else}
410411
<div class="header-row">
411-
<button type="button" class="back-button" onclick={onBack}>← Back</button>
412+
<Button variant="secondary" size="mini" onclick={onBack}>← Back</Button>
412413
<span class="host-name">{host.name}</span>
413414
<span class="share-count">{sortedShares.length} {sortedShares.length === 1 ? 'share' : 'shares'}</span>
414415
</div>
@@ -486,21 +487,6 @@
486487
margin-top: var(--spacing-sm);
487488
}
488489
489-
.btn {
490-
padding: var(--spacing-sm) var(--spacing-lg);
491-
border: 1px solid var(--color-border-strong);
492-
border-radius: var(--radius-md);
493-
background-color: var(--color-bg-secondary);
494-
color: var(--color-text-primary);
495-
font-size: var(--font-size-sm);
496-
cursor: pointer;
497-
transition: background-color var(--transition-base);
498-
}
499-
500-
.btn:hover {
501-
background-color: var(--color-bg-tertiary);
502-
}
503-
504490
.header-row {
505491
display: flex;
506492
align-items: center;
@@ -510,20 +496,6 @@
510496
border-bottom: 1px solid var(--color-border-strong);
511497
}
512498
513-
.back-button {
514-
padding: var(--spacing-xs) var(--spacing-sm);
515-
border: 1px solid var(--color-border-strong);
516-
border-radius: var(--radius-sm);
517-
background-color: transparent;
518-
color: var(--color-text-secondary);
519-
font-size: var(--font-size-sm);
520-
cursor: pointer;
521-
}
522-
523-
.back-button:hover {
524-
background-color: var(--color-bg-tertiary);
525-
}
526-
527499
.host-name {
528500
font-weight: 500;
529501
color: var(--color-text-primary);

0 commit comments

Comments
 (0)