Repository navigation
Finish the brand migration, and give the composer a focus ring - #155
Conversation
The fork re-themed the light primary button to green and stopped there, which left three measured defects rather than a matter of taste. The hover was still Cloudflare orange: touching the primary CTA swung the hue 137 degrees, green to orange, and landed on 4.30:1 against white text -- under the AA floor, at the exact moment of interaction. Dark mode shifts 1 degree, which is what correct looks like, because dark mode was never migrated at all and is internally consistent orange. Selected text was 2.81:1 (#ff500a on #ffe9e0), and that same pale orange is the composer's focus ring, so focusing the most-used control in the product drew a green border inside a pink halo. Rendering it side by side is what made that obvious; the token name gives no hint it is also a focus ring. Both themes now carry one FieldOS green at hue 152, expressed in OKLCH like the rest of the dark palette already was. Hover darkens instead of changing hue, in both themes. Every value verified: 7.52 / 9.50 light, 5.29 / 6.74 dark on white text, 8.66 for selected text -- all above 4.5. The file's own comment claimed the accent was orange while the token beneath it was green; it now says green. Adds PRODUCT.md and DESIGN.md, which record why these are defects rather than preferences, and the two-audiences constraint that upstream states outright: sales through engineering, on the same screen.
Measured on the live deployment: the composer textarea carries outline-none and nothing in its ancestor chain replaced it, so the product's primary input painted nothing a keyboard user could act on. Computed styles were byte-identical resting, after click, and after tabbing to it, while an injected control button under the same probe went from `none` to `auto 1px` -- so the probe could see focus rings, and there was none here to see. The card did have a :focus-within rule, but it only deepened an ambient drop shadow from 28px to 34px blur. That is an elevation cue, not a focus cue, and it is why a computed-style diff registered nothing. Two corrections to earlier readings of mine, both from the control. The rest of the app is fine: 14 of 14 real tab stops paint an indicator. And a probe using .click() sees no ring anywhere, because click sets :focus without :focus-visible -- the app is correctly keyboard-gated, and an earlier "no focus indicators" reading was the probe, not the app. The new rule uses :has(:focus-visible) to match that behaviour rather than lighting up on every mouse click. Also deletes .prompt-input, which is the rule that was presumably meant to do this. It sets border-color and a 3px ring on focus-within and is applied to no element anywhere in the codebase.
Reviewing my own change from earlier today against the anti-references just written into PRODUCT.md. The two empty states got different words and identical visual weight: the same sparkle icon and the same accent glow announced both "the agent has not built a UI yet" and "this gadget renders nothing". That is a weaker version of the bug the pane exists to break. The whole point was that a waiting state and a defect must be distinguishable, and they still were not -- at a glance, across a room, only by reading. It also put a sparkle on an error, in a product whose stated anti-reference is the AI-chat idiom that sparkle belongs to. The problem tone now uses the Warning icon this codebase already uses in 17 other places, on warning-tint, and drops the glow. Icon contrast measured on its own background: 5.38:1 light, 8.79:1 dark, against a 3:1 bar for non-text.
A ratio without its inputs is the same defect as a count without its scope, and this file now carries several ratios that someone will re-derive later. States the formula, which foreground/background pair backs each claim, which threshold applies and why the icon is judged at 3:1 rather than 4.5:1 -- and, importantly, what was not measured. Neutral body text and the inherited status colors carry no number here, and absence of a number is not a pass.
Verified end to end on the live deployment. Adding a model at /providers pre-fills the API URL with http://localhost:11434 -- an actual value, not placeholder text -- and addModel stores it without ever contacting it. Submitting the default unchanged produced "AI model added successfully", no failure, and a listed, selectable model indistinguishable from a working one (#156). The distinction that matters: a placeholder is a suggestion you must replace, while a pre-filled value is a form that already looks complete, so the natural action is to submit it. On this deployment the OpenAI-compatible provider is the only option, so every user takes this path, and it is the first thing a new user does. Removing the pre-fill does not add a failure mode -- it activates one that was already written. validate() at line 211 requires a non-empty apiUrl for this provider and says "Please enter the server URL"; that branch was unreachable while the field was always populated. The placeholder still carries the hint. Also fixes the doc that sends people to a 404: trying-it-out.md's first precondition says "Settings -> AI models", and there is no /settings route. It is /providers, confirmed by navigating to both. Reachability checking at add time is the other half and is left to #156, since whether an unreachable endpoint should warn or block is a product question on an airgapped install.
Two more, found by driving the model-setup flowAdding an unreachable model reports success. Verified end to end on the live deployment:
The distinction that matters: a placeholder is a suggestion you must replace; a pre-filled value is a form that already looks complete, so the natural action is to submit it. Removing the pre-fill does not add a failure mode — it activates one already written. Reachability checking is the other half and is deliberately left to #156: whether an unreachable endpoint should warn or hard-block is a product question on an airgapped install, where the address may be legitimately down at setup time.
Also recorded, not fixed
One finding is filed as an observation without a mechanism rather than a bug: clicking Delete provider produced no confirmation and left the row in place after reload. That could be a genuine defect, a confirmation step I failed to find, or a synthetic click not reaching React's handler — I could not distinguish them, so #156 says so rather than guessing. |
A design pass over
workshop-frontend. Three changes, all of them measured defects rather than taste, plus thePRODUCT.md/DESIGN.mdthat record why.1. The brand migration was half-finished
The fork re-themed the light-mode primary button to green and stopped. What was left behind:
brand→brand-hover#2d6a4f→#e03f00, a 137° hue swap green→orange#ff500aon#ffe9e0= 2.81:1#b84e00,#ff8a5cSix more orange survivors (
accent-200,selection-*,shadow-accent-*) went with them. All values are OKLCH at hue 152, matching how the dark palette was already written.Two details worth seeing rather than reading:
--color-selection-bgis also the composer's focus ring. Focusing the most-used control drew a green border inside a pink halo. The token name gives no hint it is a focus ring; rendering it side by side is what made it obvious.2. The composer had no keyboard focus indicator
Measured on the live deployment: computed styles were byte-identical resting, after click, and after tabbing to it, while an injected control button under the same probe went
none→auto 1px. The probe could see focus rings; there was none here to see.The card did have a
:focus-withinrule, but it only deepened an ambient drop shadow from 28px to 34px blur. That is an elevation cue, not a focus cue.Two corrections to my own earlier readings, both caught by the control: the rest of the app is fine (14 of 14 real tab stops paint an indicator), and a probe using
.click()sees no ring anywhere because click sets:focuswithout:focus-visible. The app is correctly keyboard-gated; an earlier "no focus indicators" reading was the probe, not the app. The new rule uses:has(:focus-visible)to match.Also deletes
.prompt-input— the rule that was presumably meant to do this job. It sets exactly the right border and ring on:focus-within, and is applied to no element anywhere in the codebase.3. The broken-gadget state looked like a waiting state
Reviewing my own change from earlier today. The two empty states got different words and identical visual weight: the same sparkle and accent glow announced both "no UI yet" and "this gadget renders nothing" — a weaker version of the exact bug that pane exists to break. It also put a sparkle on an error, in a product whose anti-reference is the AI-chat idiom sparkles belong to.
Now:
Warning(the icon already used in 17 other places) on warning-tint, no glow. Icon contrast 5.38:1 light / 8.79:1 dark, against a 3:1 bar.Context files
PRODUCT.mdandDESIGN.md, written via/impeccable teachby readingstyles.cssrather than proposing something new. The load-bearing part is a constraint upstream states outright and the design has to hold: the same screen serves sales through engineering, so neither "simplify for beginners" nor "dense tool for experts" is available.Verification
pnpm lint0 ·pnpm test0 (15 suites) · frontend build + airgap check pass ·GadgetUIsuite 15/15.Every contrast figure above is computed, not estimated. The before/after and both empty states were rendered in Chrome and looked at.