feat(app): text rendering controls in the debug bar (#57326)

This commit is contained in:
Kit Langton
2026-10-20 00:13:13 -07:00
committed by GitHub
Unverified
parent 54157a7a0b
commit 6bae1ad12b
5 changed files with 159 additions and 0 deletions
@@ -196,6 +196,7 @@ export default function DebugBar(props: {
diagnostics?: boolean
inline?: boolean
motion?: { open: boolean; toggle: () => void }
type?: { open: boolean; toggle: () => void }
}) {
const ctx = useExtension()
const router = ctx.router
@@ -758,6 +759,14 @@ export default function DebugBar(props: {
value={ctx.t(globals().slow ? "focus.on" : "focus.off")}
onClick={() => motionTuning.set(motionGlobals.slow, !globals().slow)}
/>
<ToggleCell
active={!!props.type?.open}
inline={props.inline}
label={ctx.t("type.label")}
tip={ctx.t("type.tip")}
value={ctx.t(props.type?.open ? "focus.on" : "focus.off")}
onClick={() => props.type?.toggle()}
/>
<ToggleCell
active={!!props.motion?.open}
inline={props.inline}
@@ -35,6 +35,9 @@ export default {
"motion.label": "MOTION",
"motion.title": "Motion",
"motion.tip": "Tune animation springs live. Changes persist on this device.",
"type.label": "TYPE",
"type.title": "Type",
"type.tip": "Tune text smoothing, family, weight and tracking live. Changes persist on this device.",
"speed.label": "SPEED",
"speed.tip": "Global animation speed. Click to step through 0.5× and 0.25× back to 1×.",
"slow.label": "SLOW",
@@ -8,6 +8,9 @@ const setup: Setup<typeof Debug> = (ctx) => {
// builds, prod included, drop the import and never contain knobs.
const motionTools = import.meta.env.DEV || import.meta.env.VITE_OPENCODE_CHANNEL === "beta"
const Motion = motionTools ? lazy(() => import("./motion")) : undefined
const TypePanel = motionTools ? lazy(() => import("./type-panel")) : undefined
if (motionTools) void import("./type").then((type) => type.applyStored())
// Window-local; every window starts with the bar hidden.
const [visible, setVisible] = createSignal(false)
@@ -17,6 +20,8 @@ const setup: Setup<typeof Debug> = (ctx) => {
setVisible((value) => !value)
}
const [typing, setTyping] = createSignal(false)
const toggleMotion = () => {
setMotion((value) => !value)
}
@@ -41,6 +46,7 @@ const setup: Setup<typeof Debug> = (ctx) => {
<DebugBar
diagnostics={import.meta.env.DEV}
motion={Motion ? { open: motion(), toggle: toggleMotion } : undefined}
type={TypePanel ? { open: typing(), toggle: () => setTyping((value) => !value) } : undefined}
inline
/>
</Suspense>
@@ -53,6 +59,13 @@ const setup: Setup<typeof Debug> = (ctx) => {
</Suspense>
</Show>
)}
{TypePanel && (
<Show when={visible() && typing()}>
<Suspense>
<TypePanel title={ctx.t("type.title")} />
</Suspense>
</Show>
)}
</>
),
})
@@ -0,0 +1,59 @@
import { createSignal, For } from "solid-js"
import { Portal } from "solid-js/web"
import { apply, load, options, save, type Type } from "./type"
const groups = [
["smoothing", "Smoothing"],
["family", "Family"],
["weight", "Weight"],
["tracking", "Tracking"],
["opsz", "Inter opsz"],
] as const
// The Type workspace: live text-rendering overrides, docked above the debug bar's left end. Choices persist on this device.
export default function TypePanel(props: { title: string }) {
const [type, setType] = createSignal<Type>(load())
const update = (next: Type) => {
setType(next)
apply(next)
save(next)
}
return (
<Portal>
<div class="pointer-events-auto fixed bottom-10 left-3 z-50 w-[320px] rounded-xl border border-border-base bg-surface-raised-stronger-non-alpha p-3 text-12-regular text-text-strong shadow-[var(--shadow-lg-border-base)]">
<div class="mb-2 flex items-center justify-between">
<span class="font-bold">{props.title}</span>
<button type="button" class="rounded px-1.5 hover:bg-surface-raised-base" onClick={() => update({})}>
Reset
</button>
</div>
<For each={groups}>
{([name, label]) => (
<div class="flex items-center justify-between gap-2 py-1">
<span class="opacity-70">{label}</span>
<div class="flex gap-1">
<For each={[undefined, ...options[name]]}>
{(value) => (
<button
type="button"
aria-pressed={type()[name] === value}
classList={{
"rounded px-1.5 py-0.5 hover:bg-surface-raised-base": true,
"bg-surface-raised-base font-bold": type()[name] === value,
}}
onClick={() => update({ ...type(), [name]: value })}
>
{value ?? "app"}
</button>
)}
</For>
</div>
</div>
)}
</For>
</div>
</Portal>
)
}
+75
View File
@@ -0,0 +1,75 @@
const key = "opencode.type.panel"
export const options = {
smoothing: ["auto", "antialiased"],
family: ["inter", "system"],
weight: ["400", "440", "450", "500"],
tracking: ["0", "-0.04", "-0.1"],
opsz: ["auto", "none"],
} as const
export type Type = { [K in keyof typeof options]?: (typeof options)[K][number] }
const css = `
:root[data-type-smoothing], :root[data-type-smoothing] * { -webkit-font-smoothing: var(--type-smoothing) !important; }
:root[data-type-weight] body :not(b, strong, h1, h2, h3, h4, h5, h6, th, [class*="font-bold"], [class*="font-semibold"], [class*="font-medium"], [class*="font-["]) { font-weight: var(--type-weight) !important; }
:root[data-type-tracking] body * { letter-spacing: var(--type-tracking) !important; }
:root[data-type-opsz] body * { font-optical-sizing: none !important; }
`
const system = '-apple-system, BlinkMacSystemFont, "SF Pro Text", system-ui, sans-serif'
// Overrides are global and sit on the root, so no component changes are needed. An unset group leaves the app as shipped.
export function apply(type: Type) {
const root = document.documentElement
const set = (name: string, value: string | undefined, css?: string) => {
if (value === undefined) {
root.removeAttribute(`data-type-${name}`)
root.style.removeProperty(`--type-${name}`)
return
}
root.setAttribute(`data-type-${name}`, value)
root.style.setProperty(`--type-${name}`, css ?? value)
}
if (!document.getElementById("type-tuning")) {
const style = document.createElement("style")
style.id = "type-tuning"
style.textContent = css
document.head.append(style)
}
set("smoothing", type.smoothing)
set("weight", type.weight)
set("tracking", type.tracking, type.tracking && `${type.tracking}px`)
set("opsz", type.opsz === "none" ? "none" : undefined)
for (const name of ["--font-family-sans", "--v2-font-family-sans"]) {
if (type.family === "system") root.style.setProperty(name, system)
else root.style.removeProperty(name)
}
}
export function load(): Type {
try {
return JSON.parse(localStorage.getItem(key) ?? "{}")
} catch {
return {}
}
}
export function save(type: Type) {
try {
localStorage.setItem(key, JSON.stringify(type))
} catch {}
}
export function applyStored() {
apply(load())
}