Improve wizard UX and fix CodeRabbit issues

- Add docker-options intermediate step with two choices:
  - "Download official image" (red outline) → SDK license → download
  - "Build your own image" (gray outline) → opens docs in new tab
- Update SDK license page to match other pages (back button at bottom)
- Add Wails favicon to wizard
- Fix shutdown channel panic on repeated /api/close (sync.Once guard)
- Fix Go version parsing for beta/rc versions like go1.25beta1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lea Anthony 2026-01-26 12:54:10 +11:00
commit 20f46380d4
12 changed files with 164 additions and 49 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" version="1.1" viewBox="0 0 180 167" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><g><path d="M196,19.285C196,14.712 192.095,11 187.286,11L27.714,11C22.905,11 19,14.712 19,19.285L19,158.715C19,163.288 22.905,167 27.714,167L187.286,167C192.095,167 196,163.288 196,158.715L196,19.285Z" transform="matrix(1.00565,0,0,1.05769,-18.1073,-10.6346)" style="fill:#fff"/></g><g><path d="M0,-51.891L14.429,-51.891L13.043,-21.183L22.568,-51.891L34.226,-51.891L34.084,-21.183L42.365,-51.891L56.794,-51.891L38.526,0L25.198,0L25.34,-32.45L15.211,0L1.919,0L0,-51.891Z" transform="matrix(2.51258,0,0,2.51258,20.0103,151.138)" style="fill-rule:nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 774 B

View file

@ -3,12 +3,14 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>Wails Setup Wizard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-BSN5vgpJ.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DIBmoXie.css">
<script type="module" crossorigin src="/assets/index-VD1eW7Ti.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DJ0m3TY_.css">
</head>
<body>
<div id="root"></div>

View file

@ -3,6 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<title>Wails Setup Wizard</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" version="1.1" viewBox="0 0 180 167" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><g><path d="M196,19.285C196,14.712 192.095,11 187.286,11L27.714,11C22.905,11 19,14.712 19,19.285L19,158.715C19,163.288 22.905,167 27.714,167L187.286,167C192.095,167 196,163.288 196,158.715L196,19.285Z" transform="matrix(1.00565,0,0,1.05769,-18.1073,-10.6346)" style="fill:#fff"/></g><g><path d="M0,-51.891L14.429,-51.891L13.043,-21.183L22.568,-51.891L34.226,-51.891L34.084,-21.183L42.365,-51.891L56.794,-51.891L38.526,0L25.198,0L25.34,-32.45L15.211,0L1.919,0L0,-51.891Z" transform="matrix(2.51258,0,0,2.51258,20.0103,151.138)" style="fill-rule:nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 774 B

View file

@ -12,6 +12,7 @@ type OOBEStep =
| 'deps-ready'
| 'deps-missing'
| 'cross-platform'
| 'docker-options'
| 'sdk-license'
| 'docker-setup'
| 'projects'
@ -42,6 +43,8 @@ function getWizardStage(step: OOBEStep): WizardStage {
case 'deps-missing':
return 'dependencies';
case 'cross-platform':
case 'docker-options':
case 'sdk-license':
case 'docker-setup':
return 'platform';
case 'projects':
@ -840,6 +843,87 @@ function CrossPlatformPage({
);
}
// Docker Options Page - choose between official download or build your own
function DockerOptionsPage({
onDownloadOfficial,
onSkip,
onBack,
canGoBack
}: {
onDownloadOfficial: () => void;
onSkip: () => void;
onBack?: () => void;
canGoBack?: boolean;
}) {
return (
<motion.div
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={{ duration: 0.3 }}
className="flex-1 flex flex-col items-center justify-center"
>
<div className="w-16 h-16 rounded-2xl bg-blue-500/20 flex items-center justify-center mb-6">
<svg className="w-10 h-10" viewBox="0 0 756.26 596.9">
<path fill="#1d63ed" d="M743.96,245.25c-18.54-12.48-67.26-17.81-102.68-8.27-1.91-35.28-20.1-65.01-53.38-90.95l-12.32-8.27-8.21,12.4c-16.14,24.5-22.94,57.14-20.53,86.81,1.9,18.28,8.26,38.83,20.53,53.74-46.1,26.74-88.59,20.67-276.77,20.67H.06c-.85,42.49,5.98,124.23,57.96,190.77,5.74,7.35,12.04,14.46,18.87,21.31,42.26,42.32,106.11,73.35,201.59,73.44,145.66.13,270.46-78.6,346.37-268.97,24.98.41,90.92,4.48,123.19-57.88.79-1.05,8.21-16.54,8.21-16.54l-12.3-8.27ZM189.67,206.39h-81.7v81.7h81.7v-81.7ZM295.22,206.39h-81.7v81.7h81.7v-81.7ZM400.77,206.39h-81.7v81.7h81.7v-81.7ZM506.32,206.39h-81.7v81.7h81.7v-81.7ZM84.12,206.39H2.42v81.7h81.7v-81.7ZM189.67,103.2h-81.7v81.7h81.7v-81.7ZM295.22,103.2h-81.7v81.7h81.7v-81.7ZM400.77,103.2h-81.7v81.7h81.7v-81.7ZM400.77,0h-81.7v81.7h81.7V0Z"/>
</svg>
</div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
Set up cross-compiler
</h2>
<p className="text-gray-500 dark:text-gray-400 mb-8 text-center max-w-sm">
Choose how to get the Docker cross-compilation image
</p>
<div className="flex flex-col gap-3 mb-6 w-full max-w-xs">
<button
onClick={onDownloadOfficial}
className="w-full px-5 py-3 rounded-lg border border-red-500 text-red-600 dark:text-red-400 text-sm font-medium hover:bg-red-500/10 transition-colors flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download official image
</button>
<a
href="https://wails.io/docs/guides/build/cross-platform#build-your-own-image"
target="_blank"
rel="noopener noreferrer"
className="w-full px-5 py-3 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Build your own image
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
<div className="flex flex-col items-center gap-2">
{canGoBack && onBack && (
<button
onClick={onBack}
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
>
Back
</button>
)}
<button
onClick={onSkip}
className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
>
Skip for now
</button>
</div>
</motion.div>
);
}
function DockerBuildError({
onBuildImage,
onSkip
@ -967,24 +1051,6 @@ function SDKLicensePage({
transition={{ duration: 0.3 }}
className="flex-1 flex flex-col items-center justify-center"
>
{canGoBack && onBack && (
<button
onClick={onBack}
className="absolute top-4 left-4 p-2 rounded-lg text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
aria-label="Go back"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
)}
<div className="w-12 h-12 rounded-xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-gray-600 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
Apple SDK License Agreement
</h2>
@ -1013,23 +1079,33 @@ function SDKLicensePage({
</span>
</label>
<div className="flex gap-3">
<div className="flex flex-col items-center gap-2">
<div className="flex gap-3">
{canGoBack && onBack && (
<button
onClick={onBack}
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
>
Back
</button>
)}
<button
onClick={onAgree}
disabled={!agreed}
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
agreed
? 'bg-red-500 text-white hover:bg-red-600'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Continue
</button>
</div>
<button
onClick={onDecline}
className="px-5 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
>
Skip cross-platform
</button>
<button
onClick={onAgree}
disabled={!agreed}
className={`px-5 py-2.5 rounded-lg text-sm font-medium transition-colors ${
agreed
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
Continue
Skip for now
</button>
</div>
</motion.div>
@ -1985,9 +2061,20 @@ export default function App() {
};
const handleCrossPlatformYes = async () => {
navigateTo('docker-options');
};
const handleDockerOptionsDownload = async () => {
navigateTo('sdk-license');
};
const handleDockerOptionsSkip = async () => {
const loadedDefaults = await getDefaults();
setDefaults(loadedDefaults);
setUseInterfaces(loadedDefaults.project?.useInterfaces ?? true);
navigateTo('projects');
};
const handleSDKLicenseAgree = async () => {
const docker = await getDockerStatus();
setDockerStatus(docker);
@ -2193,6 +2280,15 @@ export default function App() {
canGoBack={canGoBack}
/>
)}
{step === 'docker-options' && (
<DockerOptionsPage
key="docker-options"
onDownloadOfficial={handleDockerOptionsDownload}
onSkip={handleDockerOptionsSkip}
onBack={goBack}
canGoBack={canGoBack}
/>
)}
{step === 'sdk-license' && (
<SDKLicensePage
key="sdk-license"

View file

@ -243,6 +243,7 @@ type Wizard struct {
dockerMu sync.RWMutex
done chan struct{}
shutdown chan struct{}
shutdownOnce sync.Once
buildWg sync.WaitGroup
}
@ -510,7 +511,7 @@ func (w *Wizard) handleClose(rw http.ResponseWriter, r *http.Request) {
// Wait for any running Docker builds to complete before shutting down
go func() {
w.buildWg.Wait()
close(w.shutdown)
w.shutdownOnce.Do(func() { close(w.shutdown) })
}()
}

View file

@ -105,8 +105,20 @@ func checkGo() DependencyStatus {
versionParts := strings.Split(versionStr, ".")
if len(versionParts) >= 2 {
major, _ := strconv.Atoi(versionParts[0])
minor, _ := strconv.Atoi(versionParts[1])
major, majorErr := strconv.Atoi(versionParts[0])
// Handle versions like "25beta1" by extracting leading digits
minorStr := versionParts[1]
for i, c := range minorStr {
if c < '0' || c > '9' {
minorStr = minorStr[:i]
break
}
}
minor, minorErr := strconv.Atoi(minorStr)
if majorErr != nil || minorErr != nil {
// Couldn't parse version; assume it's acceptable
return dep
}
if major < 1 || (major == 1 && minor < 25) {
dep.Status = "needs_update"
dep.Message = "Go 1.25+ is required (found " + versionStr + ")"