Owner-approved full pass (tooltip wave, 2026-09-25). Mechanism: icon/pill controls ride the styled Radix tooltip via a new Hint composition (components/ui/hint.tsx) — the shared primitive adopted as-is, no restyling; the shortcut appends as an em-dash suffix off the LIVE registry (a rebind in Settings → Keyboard changes the suffix): "Dictate — ⌘D". Surface-owned keys (Esc closes, Enter submits) ride a raw binding prop. Native title= stays only for truncation-reveals (dock spans, the rename field) and menu-interior abbreviations (MenuShell untouched). One em-dash pattern; per-state truth on the send square (busy = "Stop the reply — Esc", idle = "Send — ↵"), Enter on the plan verdict's accept, "Stop dictation (Esc)" kept verbatim per the owner's flag. Copy: the dev-speak cull — no pipeline/prober/tier/wave vocabulary anywhere a user reads. New pins in tests/tooltip-mechanism.test.tsx (hintText composition, asChild no-wrapper-DOM, the auto-update hint constant); dock truncation titles pinned native; graph-chrome + settings pins updated to the new copy. Live-verified on :3001 — 12.6ms hover appearance (no native 1s lag); glyphs render on every suffixed surface (⌘D, ⌘K, ⌘⇧F, ⌘,, ⌘B, F2, ↵, Esc); per-page probes confirm Settings entry, Home logo, session search, Start session, Close sidebar, model + thinking pills, stats summary, rename title, plan chip, coverage ring, send square, cost button, node search (⌘F), graph gear, settings close (Close — Esc), keyboard section "Fixed — part of this screen", auto-update hint, light-mode "Coming soon", the three slot rosters, graph-tools hint. Esc still closes the settings dialog.
163 lines
5.8 KiB
TypeScript
163 lines
5.8 KiB
TypeScript
"use client";
|
|
/**
|
|
* The Appearance + Graph sections (thin settings wave).
|
|
*
|
|
* Appearance controls resolve against the factory in the provider; writes
|
|
* PUT the whole appearance section and hand the delta back so the CSS
|
|
* variables move IMMEDIATELY (the section's whole point — it must DO
|
|
* something). Hex inputs commit on blur/Enter; the background derives the
|
|
* raised/accent/edge palette in code (components/settings/appearance.tsx).
|
|
* Mode is dark-only this wave: control rendered DISABLED with a note — the
|
|
* light palette is the design wave's work, and a lying control is worse
|
|
* than an honest disabled one.
|
|
*
|
|
* 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";
|
|
import { useAppearance } from "./appearance.tsx";
|
|
import { CommitInput, SettingsRow, ThinSelect, Toggle } from "./controls.tsx";
|
|
|
|
export function AppearanceSection({
|
|
appearance,
|
|
onSectionChange,
|
|
}: {
|
|
/** The record's appearance delta (absent = factory). */
|
|
appearance: AppearanceSettings | undefined;
|
|
/** PUT the whole appearance section; the page re-applies on success. */
|
|
onSectionChange: (next: AppearanceSettings) => void;
|
|
}) {
|
|
const { appearance: resolved } = useAppearance();
|
|
const md = appearance?.mdRendering;
|
|
|
|
return (
|
|
<>
|
|
<SettingsRow label="Mode" hint="Coming soon" 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={`Empty = ${resolved.background ?? "#141414"}`}
|
|
testid="row-background"
|
|
>
|
|
<CommitInput
|
|
value={appearance?.background ?? ""}
|
|
placeholder="#141414"
|
|
ariaLabel="background hex"
|
|
testid="input-background"
|
|
onCommit={(v) => onSectionChange({ ...appearance, background: v === "" ? null : v })}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow
|
|
label="Reading font"
|
|
hint="Empty = Source Sans 3"
|
|
testid="row-font"
|
|
>
|
|
<CommitInput
|
|
value={appearance?.fontFamily ?? ""}
|
|
placeholder="Source Sans 3"
|
|
ariaLabel="font family"
|
|
testid="input-font"
|
|
onCommit={(v) => onSectionChange({ ...appearance, fontFamily: v === "" ? null : v })}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow label="Reading font size" hint={`${resolved.proseFontSize}px`} testid="row-prose-size">
|
|
<CommitInput
|
|
type="number"
|
|
value={String(appearance?.proseFontSize ?? "")}
|
|
placeholder="16"
|
|
ariaLabel="prose font size"
|
|
testid="input-prose-size"
|
|
onCommit={(v) => {
|
|
// Empty = factory default (owner, fix wave): omit the key so the record
|
|
// UNSETS and the field stays empty (absence, not null, is the
|
|
// unset for numbers). Number("") is 0, so the old clamp wrote 12.
|
|
if (v === "") {
|
|
onSectionChange({ ...appearance, proseFontSize: undefined });
|
|
return;
|
|
}
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return; // bad input: no write, factory stays
|
|
onSectionChange({ ...appearance, proseFontSize: Math.min(24, Math.max(12, Math.round(n))) });
|
|
}}
|
|
/>
|
|
</SettingsRow>
|
|
<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}`} testid="row-heading-weight">
|
|
<CommitInput
|
|
type="number"
|
|
value={String(md?.headingWeight ?? "")}
|
|
placeholder="900"
|
|
ariaLabel="heading weight"
|
|
testid="input-heading-weight"
|
|
onCommit={(v) => {
|
|
// Empty = factory default (owner, fix wave): Number("") is 0, so
|
|
// Absence is the unset representation — the old clamp wrote 100.
|
|
if (v === "") {
|
|
onSectionChange({ ...appearance, mdRendering: { ...md, headingWeight: undefined } });
|
|
return;
|
|
}
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return;
|
|
onSectionChange({
|
|
...appearance,
|
|
mdRendering: { ...md, headingWeight: Math.min(900, Math.max(100, Math.round(n))) },
|
|
});
|
|
}}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow label="Bold color" hint="Empty = #ee928c" testid="row-bold-color">
|
|
<CommitInput
|
|
value={md?.boldColor ?? ""}
|
|
placeholder="#ee928c"
|
|
ariaLabel="bold color"
|
|
testid="input-bold-color"
|
|
onCommit={(v) => onSectionChange({ ...appearance, mdRendering: { ...md, boldColor: v === "" ? null : v } })}
|
|
/>
|
|
</SettingsRow>
|
|
<SettingsRow label="Italic color" hint="Empty = #6fa9d6" testid="row-italic-color">
|
|
<CommitInput
|
|
value={md?.italicColor ?? ""}
|
|
placeholder="#6fa9d6"
|
|
ariaLabel="italic color"
|
|
testid="input-italic-color"
|
|
onCommit={(v) => onSectionChange({ ...appearance, mdRendering: { ...md, italicColor: v === "" ? null : v } })}
|
|
/>
|
|
</SettingsRow>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function GraphSection({
|
|
graph,
|
|
onSectionChange,
|
|
}: {
|
|
graph: GraphSettings | undefined;
|
|
onSectionChange: (next: GraphSettings) => void;
|
|
}) {
|
|
// Wave 7 collapse (owner ruling 2026-09-23): the section is the MASTER
|
|
// SWITCH ONLY. The embedding/clustering/labeling rungs and the BYOK
|
|
// embedding-model picker are gone — clustering is the free birth-session
|
|
// partition derived on read, labels ARE session names, and the prober's
|
|
// graph tools read the ledgers directly. There is nothing left to
|
|
// configure but on/off.
|
|
return (
|
|
<SettingsRow label="Graph" testid="graph-row-enabled">
|
|
<Toggle
|
|
checked={graph?.enabled ?? true}
|
|
label="graph enabled"
|
|
onChange={(on) => onSectionChange({ enabled: on })}
|
|
/>
|
|
</SettingsRow>
|
|
);
|
|
} |