fix(app): fit titlebar update pill to translated labels (#57422)

Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-10-20 16:51:13 +02:00
committed by GitHub
Unverified
parent 3bc6413d8c
commit 5c95abcf34
2 changed files with 65 additions and 6 deletions
@@ -0,0 +1,58 @@
import { For } from "solid-js"
import type { TitlebarItem } from "@opencode/gui-extensions/sdk"
import { TitlebarItems } from "./titlebar-items"
const catalogs = import.meta.glob<{ default: Record<string, string> }>(
"../../../../gui-extensions/src/updater/i18n/*.ts",
{ eager: true },
)
const rtl = new Set(["ar", "dv", "fa", "he", "ur"])
const locales = Object.entries(catalogs)
.map(([file, module]) => ({
locale: file.split("/").pop()!.replace(".ts", ""),
label: module.default["status.label"]!,
}))
.sort((a, b) => a.locale.localeCompare(b.locale))
const items = (label: string) => {
const item: TitlebarItem = { id: "update", label, run: () => {} }
return { ids: () => [item.id], item: () => item }
}
export default {
title: "App/Titlebar/Items",
id: "app-titlebar-items",
component: TitlebarItems,
}
/** The update pill in every updater locale; hover or focus a pill to expand its label. */
export const UpdateLabels = {
render: () => (
<div class="flex flex-col gap-1 bg-v2-background-bg-base p-4">
<For each={locales}>
{(entry) => (
<div data-locale={entry.locale} dir={rtl.has(entry.locale) ? "rtl" : "ltr"} class="flex items-center gap-4">
<span class="w-10 shrink-0 font-mono text-[11px] text-v2-text-text-weak">{entry.locale}</span>
<div
data-slot="story-horizontal"
class="flex h-10 w-[320px] shrink-0 items-center justify-end rounded-md bg-v2-background-bg-deep/60"
>
<div class="relative z-20 flex shrink-0 items-center justify-end overflow-visible">
<TitlebarItems items={items(entry.label)} />
</div>
</div>
<div
data-slot="story-vertical"
class="flex w-[220px] shrink-0 flex-col rounded-md bg-v2-background-bg-deep/60 p-2"
>
<TitlebarItems items={items(entry.label)} vertical />
</div>
</div>
)}
</For>
</div>
),
}
@@ -35,7 +35,7 @@ export function TitlebarItems(props: { items: ReturnType<typeof useTitlebarItems
function TitlebarItemButton(props: { item: TitlebarItem; vertical?: boolean }) {
const label = () => (
<span
class="shrink-0 text-[11px] leading-4 text-v2-text-text-accent [font-weight:530] opacity-0 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0"
class="shrink-0 whitespace-nowrap text-[11px] leading-4 text-v2-text-text-accent [font-weight:530] opacity-0 motion-safe:transition-all duration-150 ease-out group-hover:opacity-100 group-hover:translate-x-0 group-focus-within:opacity-100 group-focus-within:translate-x-0 motion-reduce:translate-x-0"
classList={{
"ms-px me-4 -translate-x-2 rtl:translate-x-2": props.vertical,
"ms-2 me-px translate-x-2 rtl:-translate-x-2": !props.vertical,
@@ -45,19 +45,20 @@ function TitlebarItemButton(props: { item: TitlebarItem; vertical?: boolean }) {
</span>
)
// The pill grows to fit its label, so translated labels of any length show in full.
return (
<div
data-slot="titlebar-update"
class="group relative shrink-0 rounded-full bg-v2-background-bg-deep transition-[width] duration-150 ease-out hover:z-30 focus-within:z-30 motion-reduce:transition-none"
class="group relative shrink-0 rounded-full bg-v2-background-bg-deep transition-[width] duration-150 ease-out [interpolate-size:allow-keywords] hover:z-30 hover:w-fit focus-within:z-30 focus-within:w-fit motion-reduce:transition-none"
classList={{
"h-7 w-7 self-start hover:w-[84px] focus-within:w-[84px]": props.vertical,
"me-3 h-5 w-5 hover:w-[68px] focus-within:w-[68px]": !props.vertical,
"h-7 w-7 self-start": props.vertical,
"me-3 h-5 w-5": !props.vertical,
}}
>
<button
type="button"
class="absolute top-0 z-10 flex h-full w-full items-center overflow-hidden rounded-full bg-v2-icon-icon-accent/20 text-v2-icon-icon-accent transition-[background-color] duration-150 ease-out group-hover:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] group-focus-within:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] focus-visible:outline-none disabled:opacity-60 motion-reduce:transition-none [app-region:no-drag]"
classList={{ "start-0 justify-start": props.vertical, "end-0 justify-end": !props.vertical }}
class="relative z-10 flex h-full w-full items-center overflow-hidden rounded-full bg-v2-icon-icon-accent/20 text-v2-icon-icon-accent transition-[background-color] duration-150 ease-out group-hover:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] group-focus-within:bg-[color-mix(in_srgb,var(--v2-icon-icon-accent)_20%,var(--v2-background-bg-deep))] focus-visible:outline-none disabled:opacity-60 motion-reduce:transition-none [app-region:no-drag]"
classList={{ "justify-start": props.vertical, "justify-end": !props.vertical }}
onClick={() => props.item.run?.()}
disabled={!!props.item.busy}
aria-busy={!!props.item.busy}