A filed note stays filed, and the handles are asked for
Three things that all came from chapters being thought through too late. A note put into a chapter came back out of it a few seconds later. The server had it right the whole time; every sync rebuilds the local copy from the server's answer, and the function doing that never carried chapter_id — so each sync handed back a note belonging to no chapter and the notebook took it in again. The bin's query left the column out too, which would have unfiled anything restored from it. A separator drawn in a notebook vanished when a chapter was opened. "Which chapter is open" and "which list is on screen" were one value. Opened beside or below, the list still shows the notebook — but everything around it had switched to the chapter, so the notebook's own separators were filtered away and a drag among its rows was measured against the chapter's notes, found nothing, and did nothing. And where a new note lands is now visible and yours to choose: pressing in either place makes it the one being worked in, marked with a line along its top and the word "here", and the + button names it. Rearranging becomes a mode, switched on beside the sort menu — until then no row wears a grip and none can be dragged. Filing a note is back in the row's own menu, where starring and exporting already are; the button that had appeared in every row is gone with its dialog. The account panel closes with Escape. Thirteen suites green: 335 browser checks, 241 unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4bb86bc5ac
commit
b2a5f051fe
12 files changed
+406
-291
No files matched your search
@@ -1,9 +1,17 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import * as sync from "../syncEngine";
|
||||
import { I } from "./icons.jsx";
|
||||
|
||||
/* ═══════ ACCOUNT MENU ═══════ */
|
||||
export function AccountMenu({ user, onClose, onLogout, onUserUpdate, onOpenAdmin, onExportAll, dark }) {
|
||||
/* Escape closes it, like every other panel over the app. Only clicking beside
|
||||
it worked, which on a panel this wide means aiming at a sliver of page. */
|
||||
useEffect(() => {
|
||||
const key = (e) => e.key === "Escape" && onClose();
|
||||
document.addEventListener("keydown", key);
|
||||
return () => document.removeEventListener("keydown", key);
|
||||
}, [onClose]);
|
||||
|
||||
const choosePreference = async (change) => {
|
||||
try {
|
||||
onUserUpdate(await sync.updatePreferences(change));
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { I } from "./icons.jsx";
|
||||
import { coverBackground } from "./constants.jsx";
|
||||
|
||||
/**
|
||||
* Where a note should go.
|
||||
*
|
||||
* Moving used to live only under a right-click, behind a submenu — two steps
|
||||
* before the list of places even appeared, and on a phone a long press followed
|
||||
* by a flyout. This is the same list, reached by one press of a button on the
|
||||
* row.
|
||||
*
|
||||
* Everything is on one screen: the chapters of the notebook the note is already
|
||||
* in, and then the other notebooks. Sending it to another notebook takes it out
|
||||
* of whatever chapter it was in, because a chapter belongs to the book it is in
|
||||
* — which is worth saying here rather than leaving to be discovered.
|
||||
*/
|
||||
export function MoveNoteDialog({ note, notebooks, chapters, onMove, onClose, dark }) {
|
||||
const T = useMemo(
|
||||
() => ({
|
||||
bg: dark ? "#161b22" : "#fff",
|
||||
border: dark ? "#30363d" : "#d0d7de",
|
||||
text: dark ? "#e6edf3" : "#1f2328",
|
||||
muted: dark ? "#8b949e" : "#656d76",
|
||||
accent: dark ? "#388bfd" : "#0969da",
|
||||
row: dark ? "#0d1117" : "#f6f8fa",
|
||||
hover: dark ? "#21262d" : "#eaeef2",
|
||||
}),
|
||||
[dark]
|
||||
);
|
||||
|
||||
const here = chapters.filter((c) => c.notebookId === note.folderId);
|
||||
const others = notebooks.filter((nb) => nb.id !== note.folderId);
|
||||
|
||||
const go = (folderId, chapterId) => {
|
||||
onMove(note.id, folderId, chapterId);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const Row = ({ children, onClick, disabled, dot, icon }) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
data-move-target
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
width: "100%",
|
||||
padding: "11px 12px",
|
||||
borderRadius: 10,
|
||||
border: "none",
|
||||
background: disabled ? T.row : "transparent",
|
||||
color: disabled ? T.muted : T.text,
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
fontSize: 13,
|
||||
textAlign: "left",
|
||||
}}
|
||||
onMouseEnter={(e) => !disabled && (e.currentTarget.style.background = T.hover)}
|
||||
onMouseLeave={(e) => !disabled && (e.currentTarget.style.background = "transparent")}
|
||||
>
|
||||
{dot && <div style={{ width: 12, height: 16, borderRadius: 3, background: dot, flexShrink: 0 }} />}
|
||||
{icon && <span style={{ display: "flex", opacity: 0.7, flexShrink: 0 }}>{icon}</span>}
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{children}</span>
|
||||
{disabled && <span style={{ marginLeft: "auto", fontSize: 11, flexShrink: 0 }}>here now</span>}
|
||||
</button>
|
||||
);
|
||||
|
||||
const heading = (text) => (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.6,
|
||||
textTransform: "uppercase",
|
||||
color: T.muted,
|
||||
margin: "12px 12px 4px",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-move-dialog
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
background: "rgba(0,0,0,0.5)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 4000,
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 400,
|
||||
maxHeight: "80vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: T.bg,
|
||||
border: `1px solid ${T.border}`,
|
||||
borderRadius: 16,
|
||||
padding: "20px 10px 12px",
|
||||
fontFamily: "'DM Sans',sans-serif",
|
||||
}}
|
||||
>
|
||||
<h3 style={{ margin: "0 12px 4px", fontSize: 16, fontWeight: 700, color: T.text }}>Move note</h3>
|
||||
<div
|
||||
style={{
|
||||
margin: "0 12px 8px",
|
||||
fontSize: 12,
|
||||
color: T.muted,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{note.title || "Untitled"}
|
||||
</div>
|
||||
|
||||
<div style={{ overflowY: "auto", flex: 1 }}>
|
||||
{here.length > 0 && heading("In this notebook")}
|
||||
{here.length > 0 && (
|
||||
<Row icon={I.note} disabled={!note.chapterId} onClick={() => go(note.folderId, null)}>
|
||||
The notebook itself
|
||||
</Row>
|
||||
)}
|
||||
{here.map((c) => (
|
||||
<Row
|
||||
key={c.id}
|
||||
icon={I.books}
|
||||
disabled={note.chapterId === c.id}
|
||||
onClick={() => go(note.folderId, c.id)}
|
||||
>
|
||||
{c.name}
|
||||
</Row>
|
||||
))}
|
||||
|
||||
{others.length > 0 && heading("Another notebook")}
|
||||
{others.map((nb) => (
|
||||
<Row key={nb.id} dot={coverBackground(nb.cover, nb.coverPattern)} onClick={() => go(nb.id, null)}>
|
||||
{nb.name}
|
||||
</Row>
|
||||
))}
|
||||
|
||||
{here.length === 0 && others.length === 0 && (
|
||||
<div style={{ padding: "18px 12px", fontSize: 12.5, color: T.muted }}>
|
||||
There is nowhere else to put it yet — make another notebook, or a chapter in this one.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "flex-end", padding: "10px 12px 0" }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
borderRadius: 10,
|
||||
border: `1px solid ${T.border}`,
|
||||
background: "transparent",
|
||||
color: T.text,
|
||||
cursor: "pointer",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user