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:
1 parent
ead51b9fa0
commit
2ea448f091
8 files changed
+168
-17
No files matched your search
+27
-2
@@ -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)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user