"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 (
<>
{}} testid="select-mode" disabled>
onSectionChange({ ...appearance, background: v === "" ? null : v })}
/>
onSectionChange({ ...appearance, fontFamily: v === "" ? null : 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))) });
}}
/>
onSectionChange({ ...appearance, sessionNameSerif: on })}
/>
{
// 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))) },
});
}}
/>
onSectionChange({ ...appearance, mdRendering: { ...md, boldColor: v === "" ? null : v } })}
/>
onSectionChange({ ...appearance, mdRendering: { ...md, italicColor: v === "" ? null : v } })}
/>
>
);
}
const LADDER: readonly {
key: "enabled" | "embedding" | "clustering" | "labeling";
label: string;
needs: "enabled" | "embedding" | "clustering" | "labeling" | null;
}[] = [
{ 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({
graph,
onSectionChange,
}: {
graph: GraphSettings | undefined;
onSectionChange: (next: GraphSettings) => void;
}) {
// Stored values (the switches show these) vs EFFECTIVE values (a
// stored-on rung under a broken chain is inert — the schema's ladder).
const stored = {
enabled: graph?.enabled ?? true,
embedding: graph?.embedding ?? true,
clustering: graph?.clustering ?? true,
labeling: graph?.labeling ?? true,
};
const effective = {
enabled: stored.enabled,
embedding: stored.enabled && stored.embedding,
clustering: stored.enabled && stored.embedding && stored.clustering,
labeling: stored.enabled && stored.embedding && stored.clustering && stored.labeling,
};
return (
<>
{LADDER.map(({ key, label, needs }) => {
// Touchable only when every ancestor is EFFECTIVELY on.
const parentOn = needs === null ? true : effective[needs];
return (
onSectionChange({ ...graph, [key]: on })}
/>
);
})}
>
);
}