A bigger face, and the theme where the settings are

The picture fills the account button now — twenty pixels of a face is a
smudge — and light or dark moved out of the sidebar corner into Account →
Profile, next to the typeface and how a chapter opens, which is the kind of
thing it is.

Moving it made it worth keeping: it was pure screen state, so every reload
landed on dark whatever had been chosen. It is a setting on the account now,
like the typeface, with a copy in the browser so the page does not start in
one theme and turn to the other once the account arrives.

The check I first wrote for the sidebar losing its switch could not fail. It
counts the buttons in that corner instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-22 00:10:13 +02:00
1 parent ead51b9fa0
commit 2ea448f091
8 files changed
+168 -17

No files matched your search

+27 -2
View File
@@ -96,7 +96,19 @@ async function claimProviderSignIn(reportFailure) {
* to `useNoteData`. Rendering is delegated to the three panes below.
*/
export default function App() {
const [dark, setDark] = useState(true);
/* The theme lives on the account, and a copy lives here.
*
* The account is the truth — it should follow you to the phone, like the
* typeface — but it arrives a moment after the page does, and a page that
* starts dark and turns light is worse than one that guesses right. So the
* browser remembers what it drew last time and starts there. */
const [dark, setDark] = useState(() => {
try {
return localStorage.getItem("inknote_theme") !== "light";
} catch {
return true; // private mode: the default is as good as anything
}
});
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [syncStatus, setSyncStatus] = useState("synced");
@@ -171,6 +183,20 @@ export default function App() {
useEffect(() => sync.onSyncStatus(setSyncStatus), []);
// What the account says wins as soon as it is known.
useEffect(() => {
const theme = user?.preferences?.theme;
if (theme) setDark(theme === "dark");
}, [user?.preferences?.theme]);
useEffect(() => {
try {
localStorage.setItem("inknote_theme", dark ? "dark" : "light");
} catch {
/* nothing to remember with; the account still holds it */
}
}, [dark]);
/* Why a sign-in through the provider failed, shown on the sign-in screen —
the failure happens while the browser is away, so there is nobody left
holding the request to report it to. */
@@ -1001,7 +1027,6 @@ export default function App() {
T={T}
user={user}
dark={dark}
onToggleDark={() => setDark(!dark)}
isMobile={isMobile}
open={sidebarOpen}
onClose={() => setSidebarOpen(false)}
+2
View File
@@ -165,6 +165,8 @@ export type ChapterView = "column" | "inline";
export interface UserPreferences {
noteFont: NoteFont;
chapterView?: ChapterView;
/** Light or dark, kept on the account so it follows you to the next device. */
theme?: "dark" | "light";
}
export interface User {
+42 -1
View File
@@ -24,6 +24,7 @@ export function AccountMenu({ user, onClose, onLogout, onUserUpdate, onOpenAdmin
};
const chooseFont = (noteFont) => choosePreference({ noteFont });
const chooseChapterView = (chapterView) => choosePreference({ chapterView });
const chooseTheme = (theme) => choosePreference({ theme });
const [tab, setTab] = useState("profile"); // profile | username | password
/* Two different things. `viaProvider` — this account has no password here at
all, so the tab for changing one would lead to a form that can only fail;
@@ -178,7 +179,7 @@ export function AccountMenu({ user, onClose, onLogout, onUserUpdate, onOpenAdmin
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
<Avatar user={user} size={28} />
<Avatar user={user} size={34} />
<h3
style={{
margin: 0,
@@ -325,6 +326,46 @@ export function AccountMenu({ user, onClose, onLogout, onUserUpdate, onOpenAdmin
</div>
)}
{/* Light or dark. It used to be a sun in the corner of the sidebar,
reached for by accident and forgotten on the next device; it is
a setting, so it lives with the settings and on the account. */}
<div style={{ marginTop: 22 }}>
<label style={{ fontSize: 12, color: T.muted, marginBottom: 6, display: "block" }}>
Appearance
</label>
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 8 }}>
{[
["dark", "Dark", I.moon],
["light", "Light", I.sun],
].map(([value, label, icon]) => {
const active = (user.preferences?.theme || "dark") === value;
return (
<button
key={value}
data-theme-option={value}
onClick={() => chooseTheme(value)}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
padding: "10px 6px",
borderRadius: 10,
border: `1.5px solid ${active ? T.accent : T.border}`,
background: active ? T.accent + "15" : "transparent",
color: active ? T.accent : T.text,
cursor: "pointer",
fontSize: 13,
}}
>
<span style={{ display: "flex" }}>{icon}</span>
{label}
</button>
);
})}
</div>
</div>
{/* Kept on the account rather than in this browser: a choice about
how your own notes are set should follow you to the phone, not
be made again on every device. The export is untouched — this
+17 -9
View File
@@ -130,8 +130,6 @@ function InstallRow({ T, canInstall, reason, onInstall }) {
export function Sidebar({
T,
user,
dark,
onToggleDark,
isMobile,
open,
onClose,
@@ -215,14 +213,24 @@ export function Sidebar({
</span>
</div>
<div style={{ display: "flex", gap: 4 }}>
<button onClick={onToggleDark} style={iconButton(T)}>
{dark ? I.sun : I.moon}
</button>
<button onClick={onOpenAccount} title="Account" style={iconButton(T)}>
{/* Light or dark used to live here, next to the account. It is a
setting, not something reached for while writing, and it now sits
with the other settings — which leaves this corner to the one
button that is a place to go. */}
<button
onClick={onOpenAccount}
title="Account"
style={
user?.hasAvatar
? { ...iconButton(T), width: 34, height: 34, padding: 0, borderRadius: "50%" }
: iconButton(T)
}
>
{/* The picture instead of the person icon, for an account that has
one. Not initials for the rest: the icon is what this button
has always been, and only some accounts bring a face. */}
{user?.hasAvatar ? <Avatar user={user} size={20} /> : I.user}
one — as large as the button, since a face at twenty pixels is
a smudge. Not initials for the rest: the icon is what this
button has always been, and only some accounts bring a face. */}
{user?.hasAvatar ? <Avatar user={user} size={34} /> : I.user}
</button>
</div>
</div>