diff options
| author | Adam <[email protected]> | 2026-02-12 07:16:30 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-12 07:16:30 -0600 |
| commit | 5f421883a8aa92338bee1399532f359c5e986f41 (patch) | |
| tree | 52bf13cf30e6b6f9706475818528dca280d5b1e3 | |
| parent | fa97475ee82eaca292a72baa01d7da0ef1695f1b (diff) | |
| download | opencode-5f421883a8aa92338bee1399532f359c5e986f41.tar.gz opencode-5f421883a8aa92338bee1399532f359c5e986f41.zip | |
chore: style loading screen
| -rw-r--r-- | packages/desktop/src/index.tsx | 13 | ||||
| -rw-r--r-- | packages/desktop/src/loading.tsx | 136 | ||||
| -rw-r--r-- | packages/desktop/src/styles.css | 10 | ||||
| -rw-r--r-- | packages/ui/src/components/progress.css | 63 | ||||
| -rw-r--r-- | packages/ui/src/components/progress.tsx | 39 | ||||
| -rw-r--r-- | packages/ui/src/styles/index.css | 1 | ||||
| -rw-r--r-- | packages/ui/src/styles/theme.css | 2 | ||||
| -rw-r--r-- | packages/ui/src/theme/themes/oc-1.json | 2 |
8 files changed, 179 insertions, 87 deletions
diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index ca603da5f..620914dd7 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -1,14 +1,7 @@ // @refresh reload import { webviewZoom } from "./webview-zoom" import { render } from "solid-js/web" -import { - AppBaseProviders, - AppInterface, - PlatformProvider, - Platform, - DisplayBackend, - useCommand, -} from "@opencode-ai/app" +import { AppBaseProviders, AppInterface, PlatformProvider, Platform, useCommand } from "@opencode-ai/app" import { open, save } from "@tauri-apps/plugin-dialog" import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link" import { openPath as openerOpenPath } from "@tauri-apps/plugin-opener" @@ -29,7 +22,7 @@ import { UPDATER_ENABLED } from "./updater" import { initI18n, t } from "./i18n" import pkg from "../package.json" import "./styles.css" -import { commands, InitStep, type WslConfig } from "./bindings" +import { commands, InitStep } from "./bindings" import { Channel } from "@tauri-apps/api/core" import { createMenu } from "./menu" @@ -487,11 +480,9 @@ type ServerReadyData = { url: string; password: string | null } // Gate component that waits for the server to be ready function ServerGate(props: { children: (data: Accessor<ServerReadyData>) => JSX.Element }) { const [serverData] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any)) - if (serverData.state === "errored") throw serverData.error return ( - // Not using suspense as not all components are compatible with it (undefined refs) <Show when={serverData.state !== "pending" && serverData()} fallback={ diff --git a/packages/desktop/src/loading.tsx b/packages/desktop/src/loading.tsx index a1d537a00..ee2982722 100644 --- a/packages/desktop/src/loading.tsx +++ b/packages/desktop/src/loading.tsx @@ -3,87 +3,95 @@ import { MetaProvider } from "@solidjs/meta" import "@opencode-ai/app/index.css" import { Font } from "@opencode-ai/ui/font" import { Splash } from "@opencode-ai/ui/logo" +import { Progress } from "@opencode-ai/ui/progress" import "./styles.css" -import { createSignal, Match, onCleanup, onMount } from "solid-js" +import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { commands, events, InitStep } from "./bindings" import { Channel } from "@tauri-apps/api/core" -import { Switch } from "solid-js" const root = document.getElementById("root")! +const lines = ["Just a moment...", "Migrating your database", "This may take a couple of minutes"] +const delays = [3000, 9000] render(() => { - let splash!: SVGSVGElement - const [state, setState] = createSignal<InitStep | null>(null) + const [step, setStep] = createSignal<InitStep | null>(null) + const [line, setLine] = createSignal(0) + const [percent, setPercent] = createSignal(0) + + const phase = createMemo(() => step()?.phase) + + const value = createMemo(() => { + if (phase() === "done") return 100 + return Math.max(25, Math.min(100, percent())) + }) const channel = new Channel<InitStep>() - channel.onmessage = (e) => setState(e) - commands.awaitInitialization(channel as any).then(() => { - const currentOpacity = getComputedStyle(splash).opacity - - splash.style.animation = "none" - splash.style.animationPlayState = "paused" - splash.style.opacity = currentOpacity - - requestAnimationFrame(() => { - splash.style.transition = "opacity 0.3s ease" - requestAnimationFrame(() => { - splash.style.opacity = "1" + channel.onmessage = (next) => setStep(next) + commands.awaitInitialization(channel as any).catch(() => undefined) + + createEffect(() => { + if (phase() !== "sqlite_waiting") return + + setLine(0) + setPercent(0) + + const timers = delays.map((ms, i) => setTimeout(() => setLine(i + 1), ms)) + + let stop: (() => void) | undefined + let active = true + + void events.sqliteMigrationProgress + .listen((e) => { + if (e.payload.type === "InProgress") setPercent(Math.max(0, Math.min(100, e.payload.value))) + if (e.payload.type === "Done") setPercent(100) }) + .then((unlisten) => { + if (active) { + stop = unlisten + return + } + + unlisten() + }) + .catch(() => undefined) + + onCleanup(() => { + active = false + timers.forEach(clearTimeout) + stop?.() }) }) + createEffect(() => { + if (phase() !== "done") return + + const timer = setTimeout(() => events.loadingWindowComplete.emit(null), 1000) + onCleanup(() => clearTimeout(timer)) + }) + + const status = createMemo(() => { + if (phase() === "done") return "All done" + if (phase() === "sqlite_waiting") return lines[line()] + return "Just a moment..." + }) + return ( <MetaProvider> <div class="w-screen h-screen bg-background-base flex items-center justify-center"> <Font /> - <div class="flex flex-col items-center gap-10"> - <Splash ref={splash} class="h-25 animate-[pulse-splash_2s_ease-in-out_infinite]" /> - <span class="text-text-base"> - <Switch fallback="Just a moment..."> - <Match when={state()?.phase === "done"}> - {(_) => { - onMount(() => { - setTimeout(() => events.loadingWindowComplete.emit(null), 1000) - }) - - return "All done" - }} - </Match> - <Match when={state()?.phase === "sqlite_waiting"}> - {(_) => { - const textItems = [ - "Just a moment...", - "Migrating your database", - "This could take a couple of minutes", - ] - const [textIndex, setTextIndex] = createSignal(0) - const [progress, setProgress] = createSignal(0) - - onMount(async () => { - const listener = events.sqliteMigrationProgress.listen((e) => { - if (e.payload.type === "InProgress") setProgress(e.payload.value) - }) - onCleanup(() => listener.then((c) => c())) - - await new Promise((res) => setTimeout(res, 3000)) - setTextIndex(1) - await new Promise((res) => setTimeout(res, 6000)) - setTextIndex(2) - }) - - return ( - <div class="flex flex-col items-center gap-1"> - <span>{textItems[textIndex()]}</span> - <span>Progress: {progress()}%</span> - <div class="h-2 w-48 rounded-full border border-white relative"> - <div class="bg-[#fff] h-full absolute left-0 inset-y-0" style={{ width: `${progress()}%` }} /> - </div> - </div> - ) - }} - </Match> - </Switch> - </span> + <div class="flex flex-col items-center gap-11"> + <Splash class="w-20 h-25 opacity-15" /> + <div class="w-60 flex flex-col items-center gap-4" aria-live="polite"> + <span class="w-full overflow-hidden text-center text-ellipsis whitespace-nowrap text-text-strong text-14-normal"> + {status()} + </span> + <Progress + value={value()} + class="w-20 [&_[data-slot='progress-track']]:h-1 [&_[data-slot='progress-track']]:border-0 [&_[data-slot='progress-track']]:rounded-none [&_[data-slot='progress-track']]:bg-surface-weak [&_[data-slot='progress-fill']]:rounded-none [&_[data-slot='progress-fill']]:bg-icon-warning-base" + aria-label="Database migration progress" + getValueLabel={({ value }) => `${Math.round(value)}%`} + /> + </div> </div> </div> </MetaProvider> diff --git a/packages/desktop/src/styles.css b/packages/desktop/src/styles.css index 941fb95d7..143a21312 100644 --- a/packages/desktop/src/styles.css +++ b/packages/desktop/src/styles.css @@ -5,13 +5,3 @@ button#decorum-tb-close, div[data-tauri-decorum-tb] { height: calc(var(--spacing) * 10) !important; } - -@keyframes pulse-splash { - 0%, - 100% { - opacity: 0.1; - } - 50% { - opacity: 0.3; - } -} diff --git a/packages/ui/src/components/progress.css b/packages/ui/src/components/progress.css new file mode 100644 index 000000000..c728912f7 --- /dev/null +++ b/packages/ui/src/components/progress.css @@ -0,0 +1,63 @@ +[data-component="progress"] { + display: flex; + flex-direction: column; + gap: 4px; + + [data-slot="progress-header"] { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + + [data-slot="progress-label"], + [data-slot="progress-value-label"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); + letter-spacing: var(--letter-spacing-normal); + } + + [data-slot="progress-label"] { + color: var(--text-base); + } + + [data-slot="progress-value-label"] { + color: var(--text-weak); + font-variant-numeric: tabular-nums; + } + + [data-slot="progress-track"] { + position: relative; + width: 100%; + height: 8px; + overflow: hidden; + border-radius: 999px; + border: 1px solid var(--border-weak-base); + background-color: var(--surface-base); + } + + [data-slot="progress-fill"] { + height: 100%; + width: var(--kb-progress-fill-width); + border-radius: inherit; + background-color: var(--border-active); + transition: width 200ms ease; + } + + &[data-indeterminate] [data-slot="progress-fill"] { + width: 35%; + animation: progress-indeterminate 1.3s ease-in-out infinite; + } +} + +@keyframes progress-indeterminate { + from { + transform: translateX(-100%); + } + + to { + transform: translateX(300%); + } +} diff --git a/packages/ui/src/components/progress.tsx b/packages/ui/src/components/progress.tsx new file mode 100644 index 000000000..bfe10a1d1 --- /dev/null +++ b/packages/ui/src/components/progress.tsx @@ -0,0 +1,39 @@ +import { Progress as Kobalte } from "@kobalte/core/progress" +import { Show, splitProps } from "solid-js" +import type { ComponentProps, ParentProps } from "solid-js" + +export interface ProgressProps extends ParentProps<ComponentProps<typeof Kobalte>> { + hideLabel?: boolean + showValueLabel?: boolean +} + +export function Progress(props: ProgressProps) { + const [local, others] = splitProps(props, ["children", "class", "classList", "hideLabel", "showValueLabel"]) + + return ( + <Kobalte + {...others} + data-component="progress" + classList={{ + ...(local.classList ?? {}), + [local.class ?? ""]: !!local.class, + }} + > + <Show when={local.children || local.showValueLabel}> + <div data-slot="progress-header"> + <Show when={local.children}> + <Kobalte.Label data-slot="progress-label" classList={{ "sr-only": local.hideLabel }}> + {local.children} + </Kobalte.Label> + </Show> + <Show when={local.showValueLabel}> + <Kobalte.ValueLabel data-slot="progress-value-label" /> + </Show> + </div> + </Show> + <Kobalte.Track data-slot="progress-track"> + <Kobalte.Fill data-slot="progress-fill" /> + </Kobalte.Track> + </Kobalte> + ) +} diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index c85df7ba3..167eb64c8 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -36,6 +36,7 @@ @import "../components/message-part.css" layer(components); @import "../components/message-nav.css" layer(components); @import "../components/popover.css" layer(components); +@import "../components/progress.css" layer(components); @import "../components/progress-circle.css" layer(components); @import "../components/radio-group.css" layer(components); @import "../components/resize-handle.css" layer(components); diff --git a/packages/ui/src/styles/theme.css b/packages/ui/src/styles/theme.css index 951450d54..7ecac53fe 100644 --- a/packages/ui/src/styles/theme.css +++ b/packages/ui/src/styles/theme.css @@ -510,7 +510,7 @@ --icon-success-base: var(--apple-dark-7); --icon-success-hover: var(--apple-dark-8); --icon-success-active: var(--apple-dark-11); - --icon-warning-base: var(--amber-dark-7); + --icon-warning-base: var(--amber-dark-9); --icon-warning-hover: var(--amber-dark-8); --icon-warning-active: var(--amber-dark-11); --icon-critical-base: var(--ember-dark-9); diff --git a/packages/ui/src/theme/themes/oc-1.json b/packages/ui/src/theme/themes/oc-1.json index 7dfad9ec3..54a2bf674 100644 --- a/packages/ui/src/theme/themes/oc-1.json +++ b/packages/ui/src/theme/themes/oc-1.json @@ -444,7 +444,7 @@ "icon-success-base": "var(--apple-dark-9)", "icon-success-hover": "var(--apple-dark-10)", "icon-success-active": "var(--apple-dark-11)", - "icon-warning-base": "var(--amber-dark-7)", + "icon-warning-base": "var(--amber-dark-9)", "icon-warning-hover": "var(--amber-dark-8)", "icon-warning-active": "var(--amber-dark-11)", "icon-critical-base": "var(--ember-dark-9)", |
