* feat(editor): add github releases auto update flow

Enable desktop auto-update checks and install prompt so packaged apps can update from published GitHub Releases.

* fix(canvas): keep frame labels aligned during zoom

Draw labels in viewport space with zoom-aware positioning so frame titles stay anchored while panning and zooming.

* fix(ai): disable chat interactions when no models connected

Prevent sending and shortcut actions when provider models are unavailable, and show a concise model picker hint to avoid false-available UI states.

* docs: refresh readme community and demo section

Embed the product demo video inline, add a styled Discord community entry, and update media assets for clearer README presentation.

* chore: update nitro external dependency settings

Align SSR bundling with current runtime usage by removing the unnecessary @opencode-ai external entries.

* feat(electron): enhance macOS build process and update auto-updater

- Added a new build script for macOS that allows building for both arm64 and x64 architectures.
- Updated GitHub Actions workflow to support separate build arguments for macOS arm64 and x64.
- Implemented a custom GitHub update provider for macOS to handle versioning based on architecture.
- Introduced a new icon for the electron app and removed the old logo image.

* feat(editor): enhance update ready banner with additional states and UI improvements

- Added new state management for checking updates and dismissed status.
- Improved visibility logic for the update banner based on update states.
- Enhanced UI to display current and latest version information, download progress, and error messages.
- Updated layout and styling for better user experience and accessibility.

* docs: replace stripped video embed with clickable cover link

GitHub sanitizes README HTML video tags, so the embedded demo player does not render on repository pages.

Use a clickable poster image and explicit watch link to preserve the demo entry point across GitHub README rendering.

---------

Co-authored-by: Fini <fini.yang@gmail.com>
This commit is contained in:
Kayshen Xu 2026-02-26 21:47:42 +08:00 committed by GitHub
parent 198a2685a6
commit f3f3bf19b3
2 changed files with 177 additions and 31 deletions

View file

@ -18,9 +18,12 @@ Open-source vector design tool with a **Design-as-Code** philosophy. An alternat
Design visually on an infinite canvas, generate code from designs, and let AI build entire screens from a single prompt — all in one tool that runs in your browser or as a native desktop app.
<p align="center">
<video controls playsinline preload="metadata" poster="./screenshot/op-cover.png" width="100%">
<source src="https://i.imgur.com/yiu2GTt_lq.mp4" type="video/mp4" />
</video>
<a href="https://oss.ioa.tech/zseven/openpencil/a46e24733239ce24de36702342201033.mp4">
<img src="./screenshot/op-cover.png" alt="OpenPencil demo video (click to play)" width="100%" />
</a>
</p>
<p align="center">
<a href="https://oss.ioa.tech/zseven/openpencil/a46e24733239ce24de36702342201033.mp4"><strong>▶ Watch demo video</strong></a>
</p>
## Highlights

View file

@ -1,10 +1,12 @@
import { useEffect, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { AlertCircle, CheckCircle2, Download, Loader2, RefreshCw, Sparkles } from 'lucide-react'
import { Button } from '@/components/ui/button'
export default function UpdateReadyBanner() {
const [updateState, setUpdateState] = useState<UpdaterState | null>(null)
const [isInstalling, setIsInstalling] = useState(false)
const [isChecking, setIsChecking] = useState(false)
const [dismissed, setDismissed] = useState(false)
useEffect(() => {
const updater = window.electronAPI?.updater
@ -29,6 +31,13 @@ export default function UpdateReadyBanner() {
}
}, [])
useEffect(() => {
if (!updateState) return
if (updateState.status === 'available' || updateState.status === 'downloading' || updateState.status === 'downloaded' || updateState.status === 'error') {
setDismissed(false)
}
}, [updateState])
const handleInstall = async () => {
if (!window.electronAPI?.updater) return
@ -39,39 +48,173 @@ export default function UpdateReadyBanner() {
}
}
if (!updateState || updateState.status !== 'downloaded') {
const handleCheckUpdates = async () => {
if (!window.electronAPI?.updater) return
setIsChecking(true)
try {
const next = await window.electronAPI.updater.checkForUpdates()
setUpdateState(next)
} finally {
setIsChecking(false)
}
}
if (!updateState) {
return null
}
const visible =
!dismissed
&& (updateState.status === 'checking'
|| updateState.status === 'available'
|| updateState.status === 'downloading'
|| updateState.status === 'downloaded'
|| updateState.status === 'error')
if (!visible) {
return null
}
const progress = Math.max(0, Math.min(100, Math.round(updateState.downloadProgress ?? 0)))
const releaseDate = updateState.releaseDate
? new Date(updateState.releaseDate).toLocaleDateString()
: null
const titleByStatus: Partial<Record<UpdaterStatus, string>> = {
checking: 'Checking for updates',
available: 'Update found',
downloading: 'Downloading update',
downloaded: 'Ready to install',
error: 'Update failed',
}
const subtitleByStatus: Partial<Record<UpdaterStatus, string>> = {
checking: 'Looking for the latest release...',
available: updateState.latestVersion ? `Version ${updateState.latestVersion} is available.` : 'A new version is available.',
downloading: updateState.latestVersion
? `Version ${updateState.latestVersion} is downloading in the background.`
: 'Downloading update package in the background.',
downloaded: updateState.latestVersion
? `Version ${updateState.latestVersion} has been downloaded.`
: 'The update has been downloaded.',
error: updateState.error || 'Unable to check or download the update.',
}
return (
<div className="fixed top-6 left-1/2 -translate-x-1/2 z-50 app-region-no-drag">
<div className="w-[760px] max-w-[calc(100vw-32px)] rounded-2xl border border-border bg-card/95 backdrop-blur-md shadow-2xl px-6 py-5 sm:px-8 sm:py-6 flex items-center justify-between gap-5 sm:gap-8">
<div className="min-w-0">
<p className="text-2xl sm:text-3xl font-semibold text-card-foreground leading-tight">
Update Ready To Install
</p>
<p className="mt-2 text-sm sm:text-lg text-muted-foreground">
{updateState.latestVersion ? `Version ${updateState.latestVersion} has been downloaded.` : 'A new version has been downloaded.'}
</p>
<p className="text-sm sm:text-lg text-muted-foreground">
Can take 10-15 seconds for the app to relaunch automatically.
</p>
<div className="fixed top-5 right-5 z-50 app-region-no-drag">
<div className="w-[460px] max-w-[calc(100vw-24px)] rounded-2xl border border-border/80 bg-card/95 backdrop-blur-md shadow-2xl overflow-hidden">
<div className="px-5 py-4 border-b border-border/70 bg-gradient-to-r from-foreground/5 to-transparent">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.16em] text-muted-foreground">
<Sparkles className="h-3.5 w-3.5" />
Software Update
</div>
<p className="mt-1 text-base font-semibold text-card-foreground">
{titleByStatus[updateState.status] || 'Software Update'}
</p>
<p className="mt-1 text-sm text-muted-foreground leading-relaxed break-words">
{subtitleByStatus[updateState.status]}
</p>
</div>
<button
type="button"
onClick={() => setDismissed(true)}
className="h-7 px-2 rounded-md text-xs text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
>
Dismiss
</button>
</div>
</div>
<Button
onClick={handleInstall}
disabled={isInstalling}
className="h-11 sm:h-12 px-4 sm:px-6 text-sm sm:text-lg font-semibold whitespace-nowrap bg-foreground text-background hover:bg-foreground/90"
>
{isInstalling
? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Installing...
</>
)
: 'Restart & Install'}
</Button>
<div className="px-5 py-4 space-y-3">
<div className="grid grid-cols-2 gap-2 text-xs text-muted-foreground">
<div className="rounded-lg border border-border/70 px-3 py-2">
<span className="block text-[10px] uppercase tracking-wider mb-1">Current</span>
<span className="text-foreground font-medium">{updateState.currentVersion || 'Unknown'}</span>
</div>
<div className="rounded-lg border border-border/70 px-3 py-2">
<span className="block text-[10px] uppercase tracking-wider mb-1">Latest</span>
<span className="text-foreground font-medium">{updateState.latestVersion || 'Checking...'}</span>
</div>
</div>
{(updateState.status === 'downloading' || updateState.status === 'available') && (
<div className="rounded-lg border border-border/70 p-3">
<div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
<span className="inline-flex items-center gap-1.5">
<Download className="h-3.5 w-3.5" />
Download Progress
</span>
<span className="font-medium text-foreground">{progress}%</span>
</div>
<div className="h-2 rounded-full bg-muted overflow-hidden">
<div
className="h-full bg-foreground/90 transition-all duration-300 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
)}
{updateState.status === 'error' && (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-xs text-red-700 dark:text-red-300 inline-flex items-start gap-2">
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
<span className="leading-relaxed break-words">{updateState.error || 'Unknown updater error.'}</span>
</div>
)}
{updateState.status === 'downloaded' && (
<div className="rounded-lg border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-700 dark:text-emerald-300 inline-flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 shrink-0" />
<span>Restart to apply update. Relaunch usually takes 10-15 seconds.</span>
</div>
)}
{releaseDate && (
<p className="text-[11px] text-muted-foreground">
Release date: {releaseDate}
</p>
)}
<div className="flex items-center justify-end gap-2 pt-1">
<Button
variant="outline"
onClick={handleCheckUpdates}
disabled={isChecking || isInstalling}
className="h-9"
>
{(isChecking || updateState.status === 'checking')
? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Checking...
</>
)
: (
<>
<RefreshCw className="h-4 w-4" />
Check Again
</>
)}
</Button>
<Button
onClick={handleInstall}
disabled={isInstalling || updateState.status !== 'downloaded'}
className="h-9 bg-foreground text-background hover:bg-foreground/90"
>
{isInstalling
? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Installing...
</>
)
: 'Restart & Install'}
</Button>
</div>
</div>
</div>
</div>
)