Skip to content

Commit 012f373

Browse files
AkiKurisuclaude
andcommitted
refactor(desktop): slim the app update dialog
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent f217a3d commit 012f373

10 files changed

Lines changed: 101 additions & 163 deletions

File tree

Lines changed: 83 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { useEffect, type CSSProperties } from 'react'
22
import { createPortal } from 'react-dom'
3-
import { AlertCircle, CheckCircle2, Download, ExternalLink, LoaderCircle, X } from 'lucide-react'
3+
import { AlertCircle, CheckCircle2, Download, ExternalLink } from 'lucide-react'
44

55
import type { AppUpdateState } from '../../../shared/appUpdate'
66
import { useT } from '../../contexts/LocaleContext'
77
import { MarkdownRenderer } from '../conversation/MarkdownRenderer'
88
import { Button } from '../ui/Button'
9-
import { IconButton } from '../ui/IconButton'
9+
import { ModalHeader } from '../ui/ModalHeader'
1010

1111
interface AppUpdateDialogProps {
1212
state: AppUpdateState
@@ -23,7 +23,8 @@ export function AppUpdateDialog({
2323
const update = state.update
2424
const downloading = state.status === 'downloading'
2525
const downloaded = state.status === 'downloaded'
26-
const error = state.status === 'error' ? state.error : undefined
26+
const failed = state.status === 'error'
27+
const error = failed ? state.error : undefined
2728
const canClose = !downloading && !downloaded
2829
const progress = state.progress
2930

@@ -38,6 +39,14 @@ export function AppUpdateDialog({
3839
return () => window.removeEventListener('keydown', handleKeyDown)
3940
}, [canClose, onClose])
4041

42+
const primaryLabel = downloading
43+
? t('update.downloading')
44+
: downloaded
45+
? t('update.installing')
46+
: failed
47+
? t('update.retry')
48+
: t('update.download')
49+
4150
const dialog = (
4251
<div
4352
style={backdropStyle}
@@ -53,62 +62,26 @@ export function AppUpdateDialog({
5362
aria-labelledby="app-update-title"
5463
style={dialogStyle}
5564
>
56-
<div style={closeStyle}>
57-
<IconButton
58-
label={t('update.closeAria')}
59-
icon={<X size={18} strokeWidth={2} aria-hidden="true" />}
60-
onClick={onClose}
61-
disabled={!canClose}
62-
/>
63-
</div>
64-
65-
<header style={headerStyle}>
66-
<div style={eyebrowStyle}>
67-
{downloaded ? (
68-
<CheckCircle2 size={15} strokeWidth={2} aria-hidden="true" />
69-
) : downloading ? (
70-
<LoaderCircle size={15} strokeWidth={2} aria-hidden="true" style={spinStyle} />
71-
) : (
72-
<Download size={15} strokeWidth={2} aria-hidden="true" />
73-
)}
74-
<span>
75-
{update
76-
? t('update.subtitle', { version: update.latestVersion })
77-
: t('update.checking')}
78-
</span>
79-
</div>
80-
</header>
81-
82-
<h2 id="app-update-title" style={titleStyle}>
83-
{t('update.title')}
84-
</h2>
65+
<ModalHeader
66+
icon={downloaded ? <CheckCircle2 size={18} aria-hidden /> : <Download size={18} aria-hidden />}
67+
title={t('update.title')}
68+
titleId="app-update-title"
69+
description={
70+
update ? t('update.subtitle', { version: update.latestVersion }) : t('update.checking')
71+
}
72+
onClose={canClose ? onClose : undefined}
73+
closeLabel={t('update.closeAria')}
74+
style={headerStyle}
75+
/>
8576

8677
<div style={contentStyle}>
8778
{update ? (
88-
<div style={releaseNotesStyle}>
89-
<div style={releaseNotesHeaderStyle}>
90-
<div style={releaseNotesTitleStyle}>{t('update.releaseNotes')}</div>
91-
{update.htmlUrl && (
92-
<Button
93-
variant="ghost"
94-
size="sm"
95-
style={releaseNotesActionStyle}
96-
iconLeft={<ExternalLink size={13} strokeWidth={2} aria-hidden="true" />}
97-
onClick={() => {
98-
void window.api.shell.openExternal(update.htmlUrl as string)
99-
}}
100-
>
101-
{t('update.viewRelease')}
102-
</Button>
103-
)}
104-
</div>
105-
<div style={releaseNotesBodyStyle}>
106-
<MarkdownRenderer
107-
content={update.releaseNotes || t('update.noReleaseNotes')}
108-
linkMode="external"
109-
containOverflow
110-
/>
111-
</div>
79+
<div className="app-update-notes" style={notesStyle}>
80+
<MarkdownRenderer
81+
content={update.releaseNotes || t('update.noReleaseNotes')}
82+
linkMode="external"
83+
containOverflow
84+
/>
11285
</div>
11386
) : (
11487
<p style={bodyStyle}>{t('update.checkingBody')}</p>
@@ -151,25 +124,34 @@ export function AppUpdateDialog({
151124
</div>
152125

153126
<footer style={footerStyle}>
154-
<Button
155-
variant="secondary"
156-
onClick={onClose}
157-
disabled={!canClose}
158-
>
159-
{t('update.cancel')}
160-
</Button>
161-
<Button
162-
variant="primary"
163-
onClick={onDownload}
164-
disabled={!update || downloading || downloaded}
165-
loading={downloading}
166-
>
167-
{downloading
168-
? t('update.downloading')
169-
: state.status === 'error'
170-
? t('update.retry')
171-
: t('update.download')}
172-
</Button>
127+
<div style={footerLeadStyle}>
128+
{update?.htmlUrl && (
129+
<Button
130+
variant="ghost"
131+
iconLeft={<ExternalLink size={14} strokeWidth={2} aria-hidden="true" />}
132+
onClick={() => {
133+
void window.api.shell.openExternal(update.htmlUrl as string)
134+
}}
135+
>
136+
{t('update.viewRelease')}
137+
</Button>
138+
)}
139+
</div>
140+
<div style={footerActionsStyle}>
141+
{failed && (
142+
<Button variant="secondary" onClick={onClose}>
143+
{t('update.cancel')}
144+
</Button>
145+
)}
146+
<Button
147+
variant="primary"
148+
onClick={onDownload}
149+
disabled={!update || downloading || downloaded}
150+
loading={downloading}
151+
>
152+
{primaryLabel}
153+
</Button>
154+
</div>
173155
</footer>
174156
</section>
175157
</div>
@@ -215,46 +197,15 @@ const dialogStyle: CSSProperties = {
215197
boxShadow: '0 24px 80px rgba(0, 0, 0, 0.38)'
216198
}
217199

218-
/**
219-
* The close control sits outside the header flow so its 32px footprint cannot
220-
* set the header height. Its 18px glyph then lands on the eyebrow's line and on
221-
* the dialog's 22px inset.
222-
*/
223-
const closeStyle: CSSProperties = {
224-
position: 'absolute',
225-
top: 12,
226-
right: 15,
227-
zIndex: 1
228-
}
229-
230-
// No rule under the header: the gap and the title weight already mark the edge.
231200
const headerStyle: CSSProperties = {
232-
display: 'flex',
233-
alignItems: 'center',
234-
gap: 16,
235-
padding: '20px 60px 0 22px'
236-
}
237-
238-
const eyebrowStyle: CSSProperties = {
239-
display: 'inline-flex',
240-
minWidth: 0,
241-
alignItems: 'center',
242-
gap: 7,
243-
color: 'var(--text-secondary)',
244-
fontSize: 'var(--type-secondary-size)',
245-
lineHeight: 'var(--type-secondary-line-height)'
246-
}
247-
248-
const titleStyle: CSSProperties = {
249-
margin: '6px 22px 0',
250-
fontSize: 22,
251-
lineHeight: '30px',
252-
fontWeight: 680
201+
margin: '20px 22px 0'
253202
}
254203

255204
const contentStyle: CSSProperties = {
256-
padding: '16px 22px 0',
257-
overflowY: 'auto'
205+
display: 'flex',
206+
minHeight: 0,
207+
flexDirection: 'column',
208+
padding: '0 22px'
258209
}
259210

260211
const bodyStyle: CSSProperties = {
@@ -264,54 +215,22 @@ const bodyStyle: CSSProperties = {
264215
lineHeight: 'var(--type-body-line-height)'
265216
}
266217

267-
const releaseNotesStyle: CSSProperties = {
218+
const notesStyle: CSSProperties = {
219+
minHeight: 120,
220+
maxHeight: 360,
221+
overflowY: 'auto',
222+
padding: '12px 14px',
268223
border: '1px solid var(--border-subtle)',
269224
borderRadius: 8,
270225
background: 'var(--bg-secondary)',
271-
overflow: 'hidden'
272-
}
273-
274-
/**
275-
* View release belongs beside the content it opens. The rule below the row
276-
* stays: it marks where the scrolling region begins, not where a section ends.
277-
*/
278-
const releaseNotesHeaderStyle: CSSProperties = {
279-
display: 'flex',
280-
minHeight: 36,
281-
alignItems: 'center',
282-
justifyContent: 'space-between',
283-
gap: 12,
284-
padding: '0 6px 0 11px',
285-
borderBottom: '1px solid var(--border-subtle)'
286-
}
287-
288-
const releaseNotesTitleStyle: CSSProperties = {
289-
minWidth: 0,
290-
overflow: 'hidden',
291-
color: 'var(--text-primary)',
292-
fontSize: 'var(--type-ui-size)',
293-
lineHeight: 'var(--type-ui-line-height)',
294-
fontWeight: 600,
295-
textOverflow: 'ellipsis',
296-
whiteSpace: 'nowrap'
297-
}
298-
299-
// A longer localized label must not squeeze the action out of the row.
300-
const releaseNotesActionStyle: CSSProperties = {
301-
flex: '0 0 auto'
302-
}
303-
304-
const releaseNotesBodyStyle: CSSProperties = {
305-
maxHeight: 156,
306-
overflowY: 'auto',
307-
padding: '10px 11px',
308226
color: 'var(--text-secondary)',
309227
fontSize: 'var(--type-secondary-size)',
310-
lineHeight: 'var(--type-secondary-line-height)',
228+
lineHeight: 'var(--type-secondary-prose-line-height)',
311229
overflowWrap: 'anywhere'
312230
}
313231

314232
const progressWrapStyle: CSSProperties = {
233+
flex: '0 0 auto',
315234
marginTop: 16
316235
}
317236

@@ -325,7 +244,6 @@ const progressLabelStyle: CSSProperties = {
325244
lineHeight: 'var(--type-secondary-line-height)'
326245
}
327246

328-
// Transferred bytes ride with the status label instead of a third, dimmer line.
329247
const progressBytesStyle: CSSProperties = {
330248
flex: '0 0 auto',
331249
fontVariantNumeric: 'tabular-nums'
@@ -366,14 +284,23 @@ const errorStyle: CSSProperties = {
366284
overflowWrap: 'anywhere'
367285
}
368286

369-
// No rule above the footer either; the gap carries the separation.
370287
const footerStyle: CSSProperties = {
371288
display: 'flex',
372-
justifyContent: 'flex-end',
373-
gap: 8,
289+
flex: '0 0 auto',
290+
alignItems: 'center',
291+
justifyContent: 'space-between',
292+
gap: 12,
374293
padding: '18px 22px 20px'
375294
}
376295

377-
const spinStyle: CSSProperties = {
378-
animation: 'spin 1s linear infinite'
296+
const footerLeadStyle: CSSProperties = {
297+
display: 'flex',
298+
minWidth: 0,
299+
marginLeft: -10
300+
}
301+
302+
const footerActionsStyle: CSSProperties = {
303+
display: 'flex',
304+
flex: '0 0 auto',
305+
gap: 8
379306
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* The markdown renderer sizes headings inline for conversation prose; inside the
2+
update dialog they step down so they do not outrank the dialog title. */
3+
.app-update-notes h1,
4+
.app-update-notes h2 {
5+
margin-top: 0 !important;
6+
font-size: var(--type-heading-size) !important;
7+
line-height: var(--type-heading-line-height) !important;
8+
}
9+
10+
.app-update-notes h3 {
11+
font-size: var(--type-ui-size) !important;
12+
line-height: var(--type-ui-line-height) !important;
13+
}
14+
15+
.app-update-notes .markdown-body > :last-child {
16+
margin-bottom: 0 !important;
17+
}

desktop/src/renderer/styles/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
@import "../components/settings/profile/TokenActivityHeatmap.css";
2929
@import "./shared/animations.css";
3030
@import "../components/sidebar/styles/task-drag.css";
31+
@import "../components/update/styles/app-update.css";
3132
@import "./primitives/settings-group.css";
3233
@import "./primitives/select.css";
3334
@import "./primitives/slider.css";

desktop/src/shared/locales/messages/de.ts

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/en.ts

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/es.ts

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/fr.ts

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/ja.ts

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/src/shared/locales/messages/ko.ts

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)