fix: the badge solves its lightness against the colour the browser paints

Three nits from the review round.

The contrast bisection measured the hue at full float precision, but a browser
paints 8-bit channels: rounding them moved the result up to 0.025 off the target,
which took #993388 to 4.478 against white — under the AA floor the comment
claims. Quantizing the channels inside `paintedLuminance` puts the worst case
across 2880 hue and saturation combinations at 4.500, and your own fork's badge
measures 4.53 as painted.

An agent nobody can write still offered the rename behind its path segment: an
editor with two read-only fields, in place of the click that copies the path.
It is gated on `readOnly` now, the way a detail page is gated on `onSaved`. The
script and flow editors take the same gate from the `customUi` flags that
already decide whether their summary offers one — unreachable today, since the
editors own the band only on routes that pass no `customUi`, but the four
editors now answer the question the same way.

And the registry's doc named an `onEditName` field that has never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-10-06 10:26:32 -07:00
co-authored by Claude Opus 5
parent 028d7527ff
commit ece9e67b67
5 changed files with 24 additions and 7 deletions
@@ -1582,7 +1582,13 @@
kind: 'flow',
path: pathSnapshot ?? $pathStore,
summaryContent: flowSummary,
pathTrigger: flowPathTrigger
// Gated like the summary's own editor: a host that allows neither field keeps the
// band's segment, which copies the path.
pathTrigger:
customUi?.topBar?.editablePath != false ||
customUi?.topBar?.editableSummary != false
? flowPathTrigger
: undefined
}}
actions={flowHeaderActions}
contexts={headerContexts}
@@ -2299,7 +2299,12 @@
// invalidates the band before this component is torn down.
path: pathSnapshot ?? script?.path,
summaryContent: scriptSummary,
pathTrigger: scriptPathTrigger
// Gated like the summary's own editor: a host that allows neither field keeps the
// band's segment, which copies the path.
pathTrigger:
customUi?.topBar?.editablePath != false || customUi?.topBar?.editableSummary != false
? scriptPathTrigger
: undefined
}}
afterName={scriptMarks}
actions={scriptHeaderActions}
@@ -319,7 +319,9 @@
item={{
path: pathSnapshot ?? shownPath,
summaryContent: agentSummary,
pathTrigger: agentPathTrigger
// Only where the rename behind it can be saved: a reader who cannot write this agent
// keeps the segment that copies the path, rather than an editor with nothing to edit.
pathTrigger: readOnly ? undefined : agentPathTrigger
}}
afterName={agentHint}
actions={settings}
@@ -6,7 +6,8 @@ export type PageHeaderItem = {
/** Absent for an item the picker does not file under a kind — an agent's detail page. The
* breadcrumb then names the item without claiming it is one of the three. */
kind?: WorkspaceItemKind
/** Shown whole. Clicking it opens `onEditName` where a page offers one, and copies otherwise. */
/** Shown whole. Clicking it opens whatever the page hangs off it with `pathTrigger`, and copies
* the path where there is none. */
path: string | undefined
/** Wraps the band's own path segment in whatever the page wants behind it — its rename
* popover, anchored there rather than beside the summary, so the editor opens under the half
+6 -3
View File
@@ -78,11 +78,14 @@ function contrast(a: number, b: number): number {
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05)
}
/** Luminance of what `hslFloorL` paints, not of the exact numbers: a percent of lightness is
* worth more than the margin being solved for. */
/** Luminance of what the browser paints, which is neither the exact numbers nor the `hsl()` string
* alone: the string carries whole percents, and the channels it resolves to are 8-bit. Solving
* against the unrounded channels leaves the chosen lightness up to 0.025 short of the target once
* painted, which is enough to miss AA on a hue like #993388. */
function paintedLuminance(h: number, s: number, l: number): number {
const [r, g, b] = hslToRgb(Math.round(h), Math.round(s * 100) / 100, Math.floor(l * 100) / 100)
return relativeLuminance(r, g, b)
const q = (x: number) => Math.round(x * 255) / 255
return relativeLuminance(q(r), q(g), q(b))
}
/**