mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
feat(app): text rendering controls in the debug bar (#57326)
This commit is contained in:
committed by
GitHub
Unverified
parent
54157a7a0b
commit
6bae1ad12b
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user