Five of the nine things on the list, and one finding that came out of them. Colour: a marker pen and an ink colour, written as marks here rather than installed — the schema of a note decides what can be stored in it forever. A highlight is a <mark> so a reader that drops the colour still shows the word as marked, and marked text is painted dark whatever the theme. Both travel as HTML in Markdown, like underline, and both are read back. The PDF fills the screen: the browser's own full screen for the window frame, an overlay for the sidebar and the list, Escape and a visible button for the way out. The dashboard leads with the recent notes, sorted by time whatever order the notebooks are in. A tap on the blank part of a page puts the caret at the nearest position instead of nowhere. A drag from the left edge shows the sidebar. The page has an edge on a phone, where it had none. The finding: at the speed a robot types, a long note loses keystrokes between the editor and the pagination measuring after each one — 87 of 220 paragraphs. Nothing is lost at 50 characters a second, so the page-count check now pastes its bulk text rather than measuring typing throughput. And the history suite asked for "the first note in the list" without waiting for the note to reach the server; it asks by name now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
261 lines
7.9 KiB
React
261 lines
7.9 KiB
React
import { coverBackground } from "../../ui/constants.jsx";
|
|
import { I } from "../../ui/icons.jsx";
|
|
import { NotebookCover } from "../../ui/NotebookCover.jsx";
|
|
import { fmtDate } from "../../ui/format";
|
|
import { FONT_SERIF, FONT_UI } from "../../ui/theme";
|
|
|
|
const RECENT_COUNT = 6;
|
|
|
|
/** Landing view: every notebook as a cover, plus the notes touched most recently. */
|
|
/**
|
|
* The six most recently edited, whatever order the list arrives in.
|
|
*
|
|
* `activeNotes` carries whichever arrangement the notebook it came from was in —
|
|
* a hand-made order, or a title sort — and under this heading that is simply the
|
|
* wrong list: "Recent" is a question about time, and it must answer it even when
|
|
* every notebook is arranged by hand.
|
|
*/
|
|
const byLastEdited = (notes, count) =>
|
|
[...notes].sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))).slice(0, count);
|
|
|
|
export function Dashboard({
|
|
T,
|
|
isMobile,
|
|
notebooks,
|
|
activeNotes,
|
|
activeFolder,
|
|
onToggleSidebar,
|
|
onOpenNotebook,
|
|
onOpenNote,
|
|
onNewNotebook,
|
|
onNotebookContextMenu,
|
|
dnd,
|
|
}) {
|
|
const recentlyEdited = byLastEdited(activeNotes, RECENT_COUNT);
|
|
return (
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
background: T.dashBg,
|
|
fontFamily: FONT_UI,
|
|
overflow: "auto",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
padding: isMobile ? "20px 20px 16px" : "28px 36px 20px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
<button
|
|
onClick={onToggleSidebar}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 36,
|
|
height: 36,
|
|
borderRadius: 10,
|
|
border: "none",
|
|
background: "transparent",
|
|
cursor: "pointer",
|
|
color: T.muted,
|
|
}}
|
|
>
|
|
{I.menu}
|
|
</button>
|
|
<h1
|
|
style={{
|
|
margin: 0,
|
|
fontSize: isMobile ? 20 : 24,
|
|
fontWeight: 700,
|
|
color: T.text,
|
|
fontFamily: FONT_SERIF,
|
|
}}
|
|
>
|
|
Dashboard
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ padding: isMobile ? "0 16px 24px" : "0 36px 36px" }}>
|
|
<h2
|
|
style={{
|
|
fontSize: 16,
|
|
fontWeight: 600,
|
|
color: T.text,
|
|
marginBottom: 14,
|
|
fontFamily: FONT_SERIF,
|
|
}}
|
|
>
|
|
Recent Notes
|
|
</h2>
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: isMobile ? "1fr" : "repeat(auto-fill,minmax(260px,1fr))",
|
|
gap: 12,
|
|
}}
|
|
>
|
|
{recentlyEdited.map((note) => {
|
|
const nb = notebooks.find((b) => b.id === note.folderId);
|
|
return (
|
|
<button
|
|
key={note.id}
|
|
onClick={() => onOpenNote(note)}
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
padding: "14px 16px",
|
|
borderRadius: 12,
|
|
border: `1px solid ${T.border}`,
|
|
background: T.card,
|
|
cursor: "pointer",
|
|
textAlign: "left",
|
|
}}
|
|
>
|
|
<div
|
|
style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6, width: "100%" }}
|
|
>
|
|
{nb && (
|
|
<div
|
|
style={{
|
|
width: 14,
|
|
height: 10,
|
|
borderRadius: 2,
|
|
background: coverBackground(nb.cover, nb.coverPattern),
|
|
flexShrink: 0,
|
|
}}
|
|
/>
|
|
)}
|
|
<span
|
|
style={{
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
color: T.text,
|
|
flex: 1,
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{note.title}
|
|
</span>
|
|
{note.starred && <span style={{ color: "#f0b429", fontSize: 11 }}>⭐</span>}
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 6 }}>
|
|
<span style={{ fontSize: 11, color: T.faint }}>{fmtDate(note.updatedAt)}</span>
|
|
{nb && (
|
|
<span
|
|
style={{
|
|
fontSize: 10,
|
|
color: T.faint,
|
|
background: T.accentSoft,
|
|
padding: "1px 6px",
|
|
borderRadius: 6,
|
|
}}
|
|
>
|
|
{nb.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* The covers come second now. What you were last writing is what you
|
|
usually came back for; the shelf is for choosing where to go next. */}
|
|
<h2
|
|
style={{
|
|
fontSize: 16,
|
|
fontWeight: 600,
|
|
color: T.text,
|
|
margin: 0,
|
|
padding: isMobile ? "0 16px 14px" : "0 36px 14px",
|
|
fontFamily: FONT_SERIF,
|
|
}}
|
|
>
|
|
My Notebooks
|
|
</h2>
|
|
<div
|
|
style={{
|
|
padding: isMobile ? "0 16px 24px" : "0 36px 32px",
|
|
display: "grid",
|
|
gridTemplateColumns: isMobile
|
|
? "repeat(auto-fill,minmax(140px,1fr))"
|
|
: "repeat(auto-fill,minmax(180px,1fr))",
|
|
gap: isMobile ? "20px 16px" : "32px 28px",
|
|
justifyItems: "center",
|
|
}}
|
|
>
|
|
{notebooks.map((nb) => (
|
|
<NotebookCover
|
|
key={nb.id}
|
|
notebook={nb}
|
|
noteCount={activeNotes.filter((n) => n.folderId === nb.id).length}
|
|
onClick={() => onOpenNotebook(nb.id)}
|
|
onContextMenu={(e) => onNotebookContextMenu(e, nb)}
|
|
isActive={activeFolder === nb.id}
|
|
isDragOver={dnd.overId === nb.id}
|
|
draggable={!isMobile}
|
|
onDragStart={(e) => dnd.onDragStart(e, nb.id)}
|
|
onDragEnd={dnd.onDragEnd}
|
|
onDragOver={(e) => dnd.onDragOver(e, nb.id)}
|
|
onDrop={(e) => dnd.onDrop(e, nb.id)}
|
|
/>
|
|
))}
|
|
<button
|
|
onClick={onNewNotebook}
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
gap: 10,
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: isMobile ? 130 : 156,
|
|
height: isMobile ? 175 : 210,
|
|
borderRadius: 12,
|
|
border: `2px dashed ${T.border}`,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 8,
|
|
color: T.faint,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: "50%",
|
|
background: T.accentSoft,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
color: T.accent,
|
|
}}
|
|
>
|
|
{I.plus}
|
|
</div>
|
|
<span style={{ fontSize: 12, fontWeight: 500 }}>Add Notebook</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|