Files
InkNote/frontend/src/ui/NotebookModals.jsx
T
marcandClaude Opus 5 9e4677c70b Patterns on the covers, a search box that is simply there, roomier margin notes
Four small things about the shape of the app.

Covers can be patterns as well as colours — ruled paper, graph paper,
pinstripe, bands, dots, cross-hatch, linen, washes, a ledger — drawn
entirely with CSS gradient layers. No image files, nothing to upload,
crisp at any size, and offline like everything else. A cover is stored as
an index into that list, so the new ones are appended and never inserted:
renumbering would silently repaint every notebook anyone already has.

Two things the patterns taught. One idea per cover: two crossed diagonals
meant as a chevron came out as noisy plaid at the size a cover is
actually read, and were replaced with plain bands. And the grid has to
scroll — with 27 covers the dialog ran past the bottom of a laptop screen
and took the Create button with it, which a check now holds at 1440x800.

The search box in a notebook is simply there now, and the button that
used to open it is gone. A magnifying glass cost a click and having to
remember that searching was possible at all; a search box is one line and
says for itself what it does.

Margin notes went from 208x74 to 288x112 — from three short lines to
about five. Three lines is a caption, not a remark on a passage, and the
space beside the page was sitting mostly empty.

And the notebook edit dialog was still part German, and would not say
what the server's default number of versions actually is. It names it
now, read from the server rather than written into the interface: it is
set with MAX_VERSIONS_PER_NOTE, and a hardcoded "50" would quietly start
lying the day that changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:29:00 +02:00

340 lines
10 KiB
React

import { useMemo, useState } from "react";
import { COVERS } from "./constants.jsx";
export function EditNotebookModal({ notebook, onClose, onSave, dark, defaultMaxVersions }) {
const [name, setName] = useState(notebook.name);
const [sel, setSel] = useState(notebook.cover);
// Empty means "use the server's default", which is why it is a string here and
// not a number with a stand-in value.
const [versions, setVersions] = useState(notebook.maxVersions == null ? "" : String(notebook.maxVersions));
const save = () => {
if (!name.trim()) return;
const n = versions.trim() === "" ? null : Math.max(1, Math.round(Number(versions)));
onSave({ name: name.trim(), cover: sel, maxVersions: Number.isFinite(n) ? n : null });
};
const T = useMemo(
() => ({
bg: dark ? "#161b22" : "#fff",
border: dark ? "#30363d" : "#d0d7de",
text: dark ? "#e6edf3" : "#1f2328",
muted: dark ? "#8b949e" : "#656d76",
active: dark ? "#388bfd" : "#0969da",
}),
[dark]
);
return (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.55)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
backdropFilter: "blur(6px)",
}}
onClick={onClose}
>
<div
style={{
background: T.bg,
borderRadius: 20,
padding: 30,
width: 460,
maxWidth: "92vw",
boxShadow: "0 24px 48px rgba(0,0,0,0.3)",
border: `1px solid ${T.border}`,
}}
onClick={(e) => e.stopPropagation()}
>
<h3
style={{
margin: "0 0 22px",
color: T.text,
fontSize: 18,
fontWeight: 600,
fontFamily: "'Newsreader',Georgia,serif",
}}
>
Edit Notebook
</h3>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
autoFocus
style={{
width: "100%",
padding: "10px 14px",
borderRadius: 10,
border: `1px solid ${T.border}`,
background: dark ? "#0d1117" : "#f6f8fa",
color: T.text,
fontSize: 14,
outline: "none",
marginBottom: 16,
boxSizing: "border-box",
}}
onKeyDown={(e) => {
if (e.key === "Enter") save();
}}
/>
<div style={{ marginBottom: 18 }}>
<label style={{ fontSize: 12, color: T.muted, display: "block", marginBottom: 5 }}>
Versions per note
</label>
<input
type="number"
min="1"
value={versions}
onChange={(e) => setVersions(e.target.value)}
placeholder={defaultMaxVersions ? String(defaultMaxVersions) : "Default"}
aria-label="Versions per note"
style={{
width: 110,
padding: "8px 12px",
borderRadius: 10,
border: `1px solid ${T.border}`,
background: dark ? "#0d1117" : "#f6f8fa",
color: T.text,
fontSize: 14,
outline: "none",
boxSizing: "border-box",
}}
onKeyDown={(e) => {
if (e.key === "Enter") save();
}}
/>
<div style={{ fontSize: 11, color: T.muted, marginTop: 6, lineHeight: 1.5 }}>
Older versions are only deleted once newer ones push them out — never by age. Leave it empty
for the server's default{defaultMaxVersions ? `, which is ${defaultMaxVersions}` : ""}.
</div>
</div>
<div style={{ marginBottom: 22 }}>
<div style={{ fontSize: 12, color: T.muted, marginBottom: 8 }}>Cover style</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(5,1fr)",
gap: 8,
/* Scrolls rather than growing. With patterns the list is long
enough that the dialog would run past the bottom of a laptop
screen and take the Create button with it. */
maxHeight: 268,
overflowY: "auto",
paddingRight: 4,
}}
>
{COVERS.map((c, i) => (
<button
key={i}
data-cover-option={i}
onClick={() => setSel(i)}
style={{
width: "100%",
aspectRatio: "3/4",
borderRadius: 8,
background: c.bg,
border: sel === i ? `3px solid ${T.active}` : "2px solid transparent",
cursor: "pointer",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: 4, background: c.spine }}
/>
</button>
))}
</div>
</div>
<div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
<button
onClick={onClose}
style={{
padding: "8px 20px",
borderRadius: 10,
border: `1px solid ${T.border}`,
background: "transparent",
color: T.text,
cursor: "pointer",
fontSize: 13,
fontWeight: 500,
}}
>
Cancel
</button>
<button
onClick={() => {
save();
}}
style={{
padding: "8px 20px",
borderRadius: 10,
border: "none",
background: T.active,
color: "#fff",
cursor: "pointer",
fontSize: 13,
fontWeight: 500,
}}
>
Save
</button>
</div>
</div>
</div>
);
}
export function CreateNotebookModal({ onClose, onCreate, dark }) {
const [name, setName] = useState("");
const [sel, setSel] = useState(0);
const T = useMemo(
() => ({
bg: dark ? "#161b22" : "#fff",
border: dark ? "#30363d" : "#d0d7de",
text: dark ? "#e6edf3" : "#1f2328",
muted: dark ? "#8b949e" : "#656d76",
active: dark ? "#388bfd" : "#0969da",
}),
[dark]
);
return (
<div
style={{
position: "fixed",
inset: 0,
background: "rgba(0,0,0,0.55)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
backdropFilter: "blur(6px)",
}}
onClick={onClose}
>
<div
style={{
background: T.bg,
borderRadius: 20,
padding: 30,
width: 460,
maxWidth: "92vw",
boxShadow: "0 24px 48px rgba(0,0,0,0.3)",
border: `1px solid ${T.border}`,
}}
onClick={(e) => e.stopPropagation()}
>
<h3
style={{
margin: "0 0 22px",
color: T.text,
fontSize: 18,
fontWeight: 600,
fontFamily: "'Newsreader',Georgia,serif",
}}
>
New Notebook
</h3>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Notebook name..."
autoFocus
style={{
width: "100%",
padding: "10px 14px",
borderRadius: 10,
border: `1px solid ${T.border}`,
background: dark ? "#0d1117" : "#f6f8fa",
color: T.text,
fontSize: 14,
outline: "none",
marginBottom: 16,
boxSizing: "border-box",
}}
onKeyDown={(e) => {
if (e.key === "Enter" && name.trim()) onCreate({ name: name.trim(), cover: sel });
}}
/>
<div style={{ marginBottom: 22 }}>
<div style={{ fontSize: 12, color: T.muted, marginBottom: 8 }}>Choose cover style</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(5,1fr)",
gap: 8,
/* Scrolls rather than growing. With patterns the list is long
enough that the dialog would run past the bottom of a laptop
screen and take the Create button with it. */
maxHeight: 268,
overflowY: "auto",
paddingRight: 4,
}}
>
{COVERS.map((c, i) => (
<button
key={i}
data-cover-option={i}
onClick={() => setSel(i)}
style={{
width: "100%",
aspectRatio: "3/4",
borderRadius: 8,
background: c.bg,
border: sel === i ? `3px solid ${T.active}` : "2px solid transparent",
cursor: "pointer",
position: "relative",
overflow: "hidden",
}}
>
<div
style={{ position: "absolute", left: 0, top: 0, bottom: 0, width: 4, background: c.spine }}
/>
</button>
))}
</div>
</div>
<div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
<button
onClick={onClose}
style={{
padding: "8px 20px",
borderRadius: 10,
border: `1px solid ${T.border}`,
background: "transparent",
color: T.text,
cursor: "pointer",
fontSize: 13,
fontWeight: 500,
}}
>
Cancel
</button>
<button
onClick={() => {
if (name.trim()) onCreate({ name: name.trim(), cover: sel });
}}
style={{
padding: "8px 20px",
borderRadius: 10,
border: "none",
background: T.active,
color: "#fff",
cursor: "pointer",
fontSize: 13,
fontWeight: 500,
}}
>
Create
</button>
</div>
</div>
</div>
);
}