They lived in a scratch directory under /tmp, which was cleared overnight; four of the five were simply gone. Whatever they were worth, they were worth nothing kept there. Rewritten against a shared lib.mjs. The old ones registered their users through the sign-up form, which no longer exists — each suite now creates a throwaway account through the admin API and deletes it afterwards, so a run leaves the database as it found it. The previous generation left nine accounts behind; those are gone too. Three test hooks went into the app, each of which earns its place beyond testing: aria-label on the note title (the sidebar search box is also a text input, and a screen reader could not tell them apart either), data-note-row on list rows, data-user on admin rows. Two suites were flaky in ways worth naming, because both were the test misreading the app rather than the app misbehaving: - ticking a task then inserting a note link put the link inside the task list, since Control+End lands there. Verified against the server that the tick itself persists correctly; the suite now leaves the list first and asserts where the link ended up. - the note-title lookup matched the sidebar's Trash entry, which wears the same emoji as a note row. 56 checks across five suites, all passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
223 lines
5.8 KiB
React
223 lines
5.8 KiB
React
import { I } from "../../ui/icons.jsx";
|
|
import { BackButton } from "../../ui/BackButton.jsx";
|
|
import { FONT_SERIF, FONT_UI } from "../../ui/theme";
|
|
import { NoteEditor } from "./NoteEditor.jsx";
|
|
|
|
const toolbarButton = (T, color) => ({
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 30,
|
|
height: 30,
|
|
borderRadius: 8,
|
|
border: "none",
|
|
background: "transparent",
|
|
cursor: "pointer",
|
|
color,
|
|
});
|
|
|
|
/** Shown on desktop when nothing is selected; on a phone the list fills the screen instead. */
|
|
function EmptyState({ T, isTrashView, onCreateNote }) {
|
|
return (
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
background: T.bg,
|
|
color: T.faint,
|
|
fontFamily: FONT_UI,
|
|
gap: 14,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 20,
|
|
background: T.accentSoft,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
fontSize: 32,
|
|
}}
|
|
>
|
|
✏️
|
|
</div>
|
|
<div style={{ fontSize: 15, fontWeight: 500, color: T.muted }}>Select a note to edit</div>
|
|
{!isTrashView && (
|
|
<button
|
|
onClick={onCreateNote}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "10px 22px",
|
|
borderRadius: 12,
|
|
border: "none",
|
|
background: T.accent,
|
|
color: "#fff",
|
|
cursor: "pointer",
|
|
fontSize: 13,
|
|
fontWeight: 600,
|
|
marginTop: 6,
|
|
}}
|
|
>
|
|
{I.plus} New Note
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** A trashed note can be read and restored, but not edited. */
|
|
function TrashedNote({ T, isMobile, note, onBack, onRestore, onPermanentDelete }) {
|
|
return (
|
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", background: T.bg, fontFamily: FONT_UI }}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "12px 16px",
|
|
borderBottom: `1px solid ${T.borderLight}`,
|
|
}}
|
|
>
|
|
{isMobile && <BackButton T={T} onClick={onBack} />}
|
|
<span
|
|
style={{
|
|
fontSize: 17,
|
|
fontWeight: 600,
|
|
color: T.text,
|
|
fontFamily: FONT_SERIF,
|
|
flex: 1,
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{note.title}
|
|
</span>
|
|
<button
|
|
onClick={onRestore}
|
|
style={{
|
|
padding: "6px 14px",
|
|
borderRadius: 8,
|
|
border: "none",
|
|
background: T.accent,
|
|
color: "#fff",
|
|
cursor: "pointer",
|
|
fontSize: 12,
|
|
fontWeight: 600,
|
|
}}
|
|
>
|
|
Restore
|
|
</button>
|
|
<button onClick={onPermanentDelete} style={toolbarButton(T, T.danger)}>
|
|
{I.trash}
|
|
</button>
|
|
</div>
|
|
<div style={{ flex: 1, padding: "20px 24px", color: T.muted, fontStyle: "italic", fontSize: 14 }}>
|
|
Trashed notes are read-only. Restore to edit.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The right-hand pane: title bar plus the rich-text editor.
|
|
*
|
|
* `restoreEpoch` is part of the editor's key so that restoring an old version
|
|
* remounts it on the restored body instead of trying to merge into the live
|
|
* document.
|
|
*/
|
|
export function EditorPane({
|
|
T,
|
|
dark,
|
|
isMobile,
|
|
note,
|
|
isTrashView,
|
|
restoreEpoch,
|
|
backlinks,
|
|
linkProps,
|
|
onBack,
|
|
onUpdate,
|
|
onToggleStar,
|
|
onTrash,
|
|
onRestore,
|
|
onPermanentDelete,
|
|
onOpenHistory,
|
|
onCreateNote,
|
|
}) {
|
|
if (!note)
|
|
return isMobile ? null : <EmptyState T={T} isTrashView={isTrashView} onCreateNote={onCreateNote} />;
|
|
|
|
if (isTrashView)
|
|
return (
|
|
<TrashedNote
|
|
T={T}
|
|
isMobile={isMobile}
|
|
note={note}
|
|
onBack={onBack}
|
|
onRestore={onRestore}
|
|
onPermanentDelete={onPermanentDelete}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<div style={{ flex: 1, display: "flex", flexDirection: "column", background: T.bg, fontFamily: FONT_UI }}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "8px 16px",
|
|
borderBottom: `1px solid ${T.borderLight}`,
|
|
}}
|
|
>
|
|
{isMobile && <BackButton T={T} onClick={onBack} />}
|
|
<input
|
|
type="text"
|
|
aria-label="Note title"
|
|
value={note.title}
|
|
onChange={(e) => onUpdate({ title: e.target.value })}
|
|
style={{
|
|
flex: 1,
|
|
border: "none",
|
|
background: "transparent",
|
|
outline: "none",
|
|
fontSize: isMobile ? 15 : 17,
|
|
fontWeight: 600,
|
|
color: T.text,
|
|
fontFamily: FONT_SERIF,
|
|
minWidth: 0,
|
|
}}
|
|
/>
|
|
<button onClick={onOpenHistory} title="Version history" style={toolbarButton(T, T.faint)}>
|
|
{I.history}
|
|
</button>
|
|
<button onClick={onToggleStar} style={toolbarButton(T, note.starred ? "#f0b429" : T.faint)}>
|
|
{note.starred ? I.starFill : I.star}
|
|
</button>
|
|
<button onClick={onTrash} style={toolbarButton(T, T.danger)}>
|
|
{I.trash}
|
|
</button>
|
|
</div>
|
|
<div style={{ flex: 1, overflow: "hidden" }}>
|
|
<NoteEditor
|
|
key={`${note.id}:${restoreEpoch}`}
|
|
note={note}
|
|
onUpdate={onUpdate}
|
|
dark={dark}
|
|
onOpenNote={linkProps.openLinkTarget}
|
|
searchLinkTargets={linkProps.searchLinkTargets}
|
|
resolveLinkLabel={linkProps.resolveLinkLabel}
|
|
backlinks={backlinks}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|