Settings-app refinement round 2 (owner review): toggle fix, shared model dropdown, groups, graph-view graying, pruned hints

- Toggle knob (bug report): the knob span had no left anchor, so it sat
  at its inline static position — right-edge in off, overflowing in on.
  Now left-0 + the intended 2px/18px translate; verified live: 3px from
  left when off, 1px right inset both states.
- Role model/thinking dropdowns (owner ask): the EXACT chat model
  selector dropdown, not a lookalike — the fuzzy-search + arrow-walk +
  Enter machine moved from model-picker.tsx into the shared
  components/settings/model-menu.tsx (ModelMenuList/ThinkingMenuList),
  ModelControl now renders the same bodies, and the agent roles open
  them downward with a pinned Default row (clears the override). The
  collapsed trigger keeps the thin select look (owner: 'the collapsed
  look is good'). Esc claims and closes only the menu (the dialog
  stays); a backdrop click is left to the dialog's own close.
- Sub-groups (owner ask): SettingsGroup — Models / Tools / System
  prompts inside Agent, each with a separator + quiet label; section
  headings more pronounced (text-sm, full-foreground).
- Graying (owner ask): graph-view render-panel convention — the WHOLE
  row dims (opacity-40) and the control locks; the row owns the dim so
  no double-opacity. Applied to the graph ladder, the mode row, and
  prompt rows without an override file. The 'needs X on' prose is gone.
- Hints pruned (owner ask): mechanics-explaining lines removed — the
  ladder mention, 'App-level behavior', 'Applied live as CSS
  variables', per-row explanations. Only the keyboard storage note
  stays, reworded 'Stored on this device' (desktop app — localStorage
  is the Chromium profile in Application Support, not 'this browser').

Live-verified: knob geometry both states + PUT round-trip, menu
search/pick/clear through the record, thinking set/clear, Esc layering,
ladder dim 0.4 with siblings full, pruned phrases absent. Gates:
typecheck, lint, 701/701.
This commit is contained in:
amosblomqvist
2026-09-19 22:29:17 +03:00
parent 5e21561997
commit fd63e4a575
8 changed files with 536 additions and 294 deletions
+17 -17
View File
@@ -11,8 +11,9 @@
* light palette is the design wave's work, and a lying control is worse
* than an honest disabled one.
*
* Graph is the dependency ladder (owner decision, 2026-09-19): a disabled
* parent grays its dependents — stored values persist but resolve inert.
* Graph graying (owner, 2026-09-20) follows the graph viewer's render-panel
* convention: the whole row dims when a parent is off (stored values
* persist but resolve inert — the switch shows stored, the dim effective).
*/
import type { AppearanceSettings, GraphSettings } from "@/src/core";
@@ -33,14 +34,14 @@ export function AppearanceSection({
return (
<>
<SettingsRow label="Mode" hint="Light mode ships with the design wave" testid="row-mode">
<SettingsRow label="Mode" hint="Light ships with the design wave" testid="row-mode" dimmed>
<ThinSelect value="dark" ariaLabel="mode" onChange={() => {}} testid="select-mode" disabled>
<option value="dark">Dark</option>
</ThinSelect>
</SettingsRow>
<SettingsRow
label="Background"
hint={`One color; raised/accent/edges derive from it. Empty = the dark default (${resolved.background ?? "#141414"})`}
hint={`Empty = ${resolved.background ?? "#141414"}`}
testid="row-background"
>
<CommitInput
@@ -53,7 +54,7 @@ export function AppearanceSection({
</SettingsRow>
<SettingsRow
label="Reading font"
hint="Font family name, by name — everything that reads today's Source Sans 3. Empty = Source Sans 3"
hint="Empty = Source Sans 3"
testid="row-font"
>
<CommitInput
@@ -64,7 +65,7 @@ export function AppearanceSection({
onCommit={(v) => onSectionChange({ ...appearance, fontFamily: v === "" ? null : v })}
/>
</SettingsRow>
<SettingsRow label="Reading font size" hint={`${resolved.proseFontSize}px — prose, inputs, session titles scale together`} testid="row-prose-size">
<SettingsRow label="Reading font size" hint={`${resolved.proseFontSize}px`} testid="row-prose-size">
<CommitInput
type="number"
value={String(appearance?.proseFontSize ?? "")}
@@ -78,14 +79,14 @@ export function AppearanceSection({
}}
/>
</SettingsRow>
<SettingsRow label="Session names in serif" hint="CMU Serif titles on/off" testid="row-session-serif">
<SettingsRow label="Session names in serif" testid="row-session-serif">
<Toggle
checked={appearance?.sessionNameSerif ?? true}
label="session names serif"
onChange={(on) => onSectionChange({ ...appearance, sessionNameSerif: on })}
/>
</SettingsRow>
<SettingsRow label="Heading weight" hint={`${resolved.mdRendering.headingWeight} — CSS resolves the nearest cut`} testid="row-heading-weight">
<SettingsRow label="Heading weight" hint={`${resolved.mdRendering.headingWeight}`} testid="row-heading-weight">
<CommitInput
type="number"
value={String(md?.headingWeight ?? "")}
@@ -102,7 +103,7 @@ export function AppearanceSection({
}}
/>
</SettingsRow>
<SettingsRow label="Bold color" hint="**Bold** spans in teaching prose. Empty = the default ink" testid="row-bold-color">
<SettingsRow label="Bold color" hint="Empty = #c96b4a" testid="row-bold-color">
<CommitInput
value={md?.boldColor ?? ""}
placeholder="#c96b4a"
@@ -111,7 +112,7 @@ export function AppearanceSection({
onCommit={(v) => onSectionChange({ ...appearance, mdRendering: { ...md, boldColor: v === "" ? null : v } })}
/>
</SettingsRow>
<SettingsRow label="Italic color" hint="*Italic* spans. Empty = the default ink" testid="row-italic-color">
<SettingsRow label="Italic color" hint="Empty = #6fa9d6" testid="row-italic-color">
<CommitInput
value={md?.italicColor ?? ""}
placeholder="#6fa9d6"
@@ -128,12 +129,11 @@ const LADDER: readonly {
key: keyof GraphSettings;
label: string;
needs: keyof GraphSettings | null;
hint: string;
}[] = [
{ key: "enabled", label: "Graph", needs: null, hint: "Master: off = no extraction, embedding, or injection" },
{ key: "embedding", label: "Embedding", needs: "enabled", hint: "Vectors + graph-context injection" },
{ key: "clustering", label: "Clustering", needs: "embedding", hint: "Cluster computation + settle" },
{ key: "labeling", label: "Labeling", needs: "clustering", hint: "The LLM cluster-namer" },
{ key: "enabled", label: "Graph", needs: null },
{ key: "embedding", label: "Embedding", needs: "enabled" },
{ key: "clustering", label: "Clustering", needs: "embedding" },
{ key: "labeling", label: "Labeling", needs: "clustering" },
];
export function GraphSection({
@@ -159,11 +159,11 @@ export function GraphSection({
};
return (
<>
{LADDER.map(({ key, label, needs, hint }) => {
{LADDER.map(({ key, label, needs }) => {
// Touchable only when every ancestor is EFFECTIVELY on.
const parentOn = needs === null ? true : effective[needs];
return (
<SettingsRow key={key} label={label} hint={!parentOn ? `${hint} — needs ${needs} on` : hint} testid={`graph-row-${key}`}>
<SettingsRow key={key} label={label} testid={`graph-row-${key}`} dimmed={!parentOn}>
<Toggle
checked={stored[key]}
disabled={!parentOn}