From 8cac48ad1af44f722b2ab2f77afe337ce4932a65 Mon Sep 17 00:00:00 2001 From: David Hill Date: Wed, 30 Sep 2026 12:47:00 -0600 Subject: [PATCH] feat(app): copy version from about settings --- packages/app/src/runtime/i18n/en.ts | 1 + packages/app/src/settings/about/about.tsx | 56 ++++++++++++++++++++--- packages/app/src/settings/settings.css | 48 +++++++++++++++++++ 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 0c44e05dad1..3f25ac58553 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -1151,6 +1151,7 @@ export const dict = { "settings.tab.about": "About", "settings.about.version": "Version {{version}}", "settings.about.devVersion": "development", + "settings.about.copyVersionFailed": "Failed to copy version", "settings.about.license": "Released under the MIT License", "settings.about.writtenBy": "Written by", "settings.about.illustratedBy": "Illustrated by", diff --git a/packages/app/src/settings/about/about.tsx b/packages/app/src/settings/about/about.tsx index 38f6fbf0b6a..f9fd81e11a9 100644 --- a/packages/app/src/settings/about/about.tsx +++ b/packages/app/src/settings/about/about.tsx @@ -1,7 +1,10 @@ -import { createResource } from "solid-js" +import { Tooltip } from "@opencode/ui/tooltip" +import { createEffect, createResource, onCleanup } from "solid-js" +import { createStore } from "solid-js/store" import { useLanguage } from "@/runtime/i18n/language" import { usePlatform } from "@/runtime/platform/platform" import { ExternalLink } from "@/runtime/platform/external-link" +import { showToast } from "@/shell/notifications/toast" import legal from "./legal.svg" import anomalyBrush from "./anomaly-brush.svg" import { AnimatedWordmark } from "./animated-wordmark" @@ -30,11 +33,36 @@ const illustrators = ["usrnk1", "ludvigrask_", "arvsrn", "iamdavidhill"] as cons export function SettingsAbout(props: { active: boolean }) { const language = useLanguage() const platform = usePlatform() + const [copy, setCopy] = createStore({ pending: false, copied: false, forceCopied: false }) + const version = () => platform.version ?? language.t("settings.about.devVersion") + const versionLabel = () => language.t("settings.about.version", { version: version() }) const [otherContributors] = createResource( () => props.active || undefined, () => loadOtherContributorCount(platform.fetch ?? fetch), { initialValue: FALLBACK_OTHER_CONTRIBUTORS }, ) + createEffect(() => { + if (!copy.copied) return + const close = setTimeout(() => setCopy("forceCopied", false), 2000) + const reset = setTimeout(() => setCopy("copied", false), 2120) + onCleanup(() => { + clearTimeout(close) + clearTimeout(reset) + }) + }) + const copyVersion = async () => { + if (copy.pending) return + setCopy({ pending: true, copied: false, forceCopied: false }) + await (platform.writeClipboardText?.(versionLabel()) ?? navigator.clipboard.writeText(versionLabel())) + .then(() => setCopy({ copied: true, forceCopied: true })) + .catch(() => + showToast({ + variant: "error", + title: language.t("settings.about.copyVersionFailed"), + }), + ) + .finally(() => setCopy("pending", false)) + } const credit = (name: string) => ( {name} @@ -52,11 +80,27 @@ export function SettingsAbout(props: { active: boolean }) { return (
-

- {language.t("settings.about.version", { - version: platform.version ?? language.t("settings.about.devVersion"), - })} -

+
+ + + +

{language.t("settings.about.license")}

diff --git a/packages/app/src/settings/settings.css b/packages/app/src/settings/settings.css index fd6c76a69d4..0a7c5de47fc 100644 --- a/packages/app/src/settings/settings.css +++ b/packages/app/src/settings/settings.css @@ -233,6 +233,54 @@ gap: 14px; } +.settings-about-version-line { + display: flex; + align-items: center; +} + +.settings-about-version { + position: relative; + isolation: isolate; + display: inline-flex; + min-height: 24px; + align-items: center; + margin-block: -2px; + padding: 0; + border: 0; + border-radius: 4px; + background: transparent; + color: inherit; + font: inherit; + cursor: pointer; + transition: color 120ms ease; +} + +.settings-about-version::before { + position: absolute; + z-index: -1; + inset-block: 0; + inset-inline: -6px; + border-radius: 4px; + background: transparent; + content: ""; + transition: background-color 120ms ease; +} + +.settings-about-version:hover, +.settings-about-version:focus-visible { + color: var(--v2-text-text-base); + outline: none; +} + +.settings-about-version:hover::before, +.settings-about-version:focus-visible::before { + background: var(--v2-background-bg-layer-02); +} + +.settings-about-version:disabled { + cursor: default; +} + .settings-about-credits, .settings-about-publication, .settings-about-details {