Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion biome.jsonc
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"files": {
"ignore": ["dist/**", "graypaper-archive/**", "public/**", "tools/matrix-bot/output/messages.json"]
"ignore": [
"dist/**",
"graypaper-archive/**",
"public/**",
"tools/matrix-bot/output/messages.json",
"tools/snapshot-tests/playwright-report/**"
]
},
"formatter": {
"enabled": true,
Expand Down
8 changes: 8 additions & 0 deletions src/components/LocationProvider/LocationProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ const BASE64_VALIDATION_REGEX = /^#[-A-Za-z0-9+/]*={0,3}$/;

export const LocationContext = createContext<ILocationContext | null>(null);

export const useLocationContext = () => {
const context = useContext(LocationContext);
if (!context) {
throw new Error("useLocationContext must be used within a LocationProvider");
}
return context;
};

export function LocationProvider({ children }: ILocationProviderProps) {
const { metadata } = useContext(MetadataContext) as IMetadataContext;
const [locationParams, setLocationParams] = useState<ILocationParams>();
Expand Down
52 changes: 14 additions & 38 deletions src/components/NoteManager/NoteManager.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
.note-manager{
--inactive-note-bg: #ebebeb;
--active-note-bg: #d8e8e7;
--active-note-shadow-bg: #c8d9d8;
}

.dark .note-manager{
--inactive-note-bg: #444;
--active-note-bg: #3A4949;
--active-note-shadow-bg: #4A6565;

}

.notes-wrapper {
height: 100%;
display: flex;
Expand Down Expand Up @@ -37,12 +50,7 @@
border: 1px solid #f22;
}

.note-manager blockquote {
padding: 0.5rem;
margin: 0.5rem 0;
padding-bottom: 16px;
white-space: pre-wrap;
}


.note-manager .icon {
margin-right: 3px;
Expand All @@ -62,42 +70,10 @@
cursor: initial;
}

.note-manager .note {
padding: 8px;
border: 1px solid;
border-color: light-dark(#eee, #444);
margin-top: 5px;
border-radius: 6px;
}

.note-manager .note-link a {
font-weight: bold;
}

.note-manager .note .actions {
display: flex;
}

.note-manager button.remove {
color: #f22;
}

.note-manager .fill {
flex: 1;
}

.note-manager button.save {
color: #2a2;
}

.note-manager button.edit {
font-size: 8px;
background: none;
filter: grayscale(100%);
opacity: 30%;
transition: all 200ms ease-in-out;
}
.note-manager .note:hover button.edit {
filter: none;
opacity: 100%;
}
11 changes: 8 additions & 3 deletions src/components/NoteManager/NoteManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function Notes() {
const [noteContent, setNoteContent] = useState("");
const [noteContentError, setNoteContentError] = useState("");
const { locationParams } = useContext(LocationContext) as ILocationContext;
const { notesReady, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
const { notesReady, activeNotes, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
NotesContext,
) as INotesContext;
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
Expand Down Expand Up @@ -74,7 +74,7 @@ function Notes() {
}, [selectedBlocks]);

return (
<div className="note-manager" style={{ opacity: notesReady ? 1.0 : 0.3 }}>
<div className="note-manager flex flex-col gap-2.5" style={{ opacity: notesReady ? 1.0 : 0.3 }}>
<div className="new-note">
<textarea
disabled={selectedBlocks.length === 0}
Expand All @@ -91,7 +91,12 @@ function Notes() {
</button>
</div>

<MemoizedNotesList notes={notes} onEditNote={handleUpdateNote} onDeleteNote={handleDeleteNote} />
<MemoizedNotesList
activeNotes={activeNotes}
notes={notes}
onEditNote={handleUpdateNote}
onDeleteNote={handleDeleteNote}
/>
</div>
);
}
Empty file.
195 changes: 151 additions & 44 deletions src/components/NoteManager/components/Note.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { type ChangeEvent, type MouseEventHandler, useCallback, useState } from "react";
import {
type ChangeEvent,
type MouseEventHandler,
createContext,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { validateMath } from "../../../utils/validateMath";
import { NoteContent } from "../../NoteContent/NoteContent";
import type { INotesContext } from "../../NotesProvider/NotesProvider";
import { type IDecoratedNote, NoteSource } from "../../NotesProvider/types/DecoratedNote";
import type { IStorageNote, UnPrefixedLabel } from "../../NotesProvider/types/StorageNote";
import { NoteLabels, NoteLabelsEdit } from "./NoteLabels";
import { NoteLink } from "./NoteLink";
import "./Note.css";
import { Button, cn } from "@fluffylabs/shared-ui";
import { useLocationContext } from "../../LocationProvider/LocationProvider";

export type NotesItem = {
location: string; // serialized InDocSelection
Expand All @@ -14,17 +25,30 @@ export type NotesItem = {

type NoteProps = {
note: IDecoratedNote;
active: boolean;
onEditNote: INotesContext["handleUpdateNote"];
onDeleteNote: INotesContext["handleDeleteNote"];
};

export function Note({ note, onEditNote, onDeleteNote }: NoteProps) {
const noteContext = createContext<IDecoratedNote | null>(null);

const useNoteContext = () => {
const context = useContext(noteContext);
if (!context) {
throw new Error("useNoteContext must be used within a NoteContextProvider");
}
return context;
};

export function Note({ note, active = false, onEditNote, onDeleteNote }: NoteProps) {
const [isEditing, setIsEditing] = useState(false);
const [noteDirty, setNoteDirty] = useState<IStorageNote>({
...note.original,
});
const [noteContentError, setNoteContentError] = useState("");

const { setLocationParams } = useLocationContext();

const isEditable = note.source !== NoteSource.Remote;

const handleEditLabels = useCallback(
Expand Down Expand Up @@ -70,49 +94,132 @@ export function Note({ note, onEditNote, onDeleteNote }: NoteProps) {
setIsEditing(false);
}, []);

const handleWholeNoteClick = (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target;

if (target instanceof Element && (target.closest("button") || target.closest("a"))) {
e.preventDefault();
return;
}

if (active) {
return;
}

setLocationParams({
version: note.original.version,
selectionStart: note.original.selectionStart,
selectionEnd: note.original.selectionEnd,
});
};

const handleNoteEnter = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key !== "Enter" && e.key !== "Space") {
e.preventDefault();
}

if (active) {
return;
}

setLocationParams({
version: note.original.version,
selectionStart: note.original.selectionStart,
selectionEnd: note.original.selectionEnd,
});
};

useEffect(() => {
if (!active) {
setIsEditing(false);
}
}, [active]);

return (
<div className="note">
<NoteLink note={note} onEditNote={onEditNote} />
{isEditing ? (
<>
<textarea
className={noteContentError ? "error" : ""}
onChange={handleNoteContentChange}
value={noteDirty.content}
autoFocus
/>
{noteContentError ? <div className="validation-message">{noteContentError}</div> : null}
</>
) : (
<blockquote>
{note.original.author}
<NoteContent content={note.original.content} />
</blockquote>
)}
{isEditing ? <NoteLabelsEdit note={note} onNewLabels={handleEditLabels} /> : null}
<div className="actions">
{!isEditing ? <NoteLabels note={note} /> : null}

{isEditing ? (
<button className="remove default-button" onClick={handleDeleteClick}>
delete
</button>
) : null}

<div className="fill" />

{isEditable ? (
<button
className={`default-button ${isEditing ? "save" : "edit"}`}
data-testid={isEditing ? "save-button" : "edit-button"}
onClick={isEditing ? handleSaveClick : handleEditClick}
>
{isEditing ? "save" : "✏️"}
</button>
) : null}

{isEditing ? <button onClick={handleCancelClick}>cancel</button> : null}
<NoteLayout.Root value={note}>
<div
data-testid="notes-manager-card"
className={cn(
"note rounded-xl p-4 flex flex-col gap-2",
active && "bg-[var(--active-note-bg)] shadow-[0px_4px_0px_1px_var(--active-note-shadow-bg)]",
!active && "bg-[var(--inactive-note-bg)] cursor-pointer",
)}
role={!active ? "button" : undefined}
tabIndex={!active ? 0 : undefined}
aria-label={!active ? "Activate label" : ""}
onClick={handleWholeNoteClick}
onKeyDown={handleNoteEnter}
>
{!active && (
<>
<NoteLink note={note} onEditNote={onEditNote} />
<NoteLayout.Text />
</>
)}
{active && !isEditing && (
<>
<div className="flex justify-between items-start">
<NoteLink note={note} onEditNote={onEditNote} />
{isEditable && (
<Button
variant="ghost"
intent="neutralStrong"
className="p-2 h-8"
data-testid={isEditing ? "save-button" : "edit-button"}
onClick={isEditing ? handleSaveClick : handleEditClick}
>
✏️
</Button>
)}
</div>
<NoteLayout.Text />
{!isEditing ? <NoteLabels note={note} /> : null}
</>
)}
{active && isEditing && (
<>
<>
<NoteLink note={note} onEditNote={onEditNote} />
<textarea
className={noteContentError ? "error" : ""}
onChange={handleNoteContentChange}
value={noteDirty.content}
autoFocus
/>
{noteContentError ? <div className="validation-message">{noteContentError}</div> : null}
<NoteLabelsEdit note={note} onNewLabels={handleEditLabels} />
<div className="actions gap-2">
<Button variant="ghost" intent="destructive" size="sm" onClick={handleDeleteClick}>
Delete
</Button>
<div className="fill" />
<Button variant="tertiary" data-testid={"cancel-button"} onClick={handleCancelClick} size="sm">
Cancel
</Button>
<Button data-testid={"save-button"} onClick={handleSaveClick} size="sm">
Save
</Button>
</div>
</>
</>
)}
</div>
</div>
</NoteLayout.Root>
);
}

const NoteText = () => {
const note = useNoteContext();

return (
<blockquote className="whitespace-pre-wrap">
{note.original.author}
<NoteContent content={note.original.content} />
</blockquote>
);
};

const NoteLayout = {
Root: noteContext.Provider,
Text: NoteText,
};
11 changes: 8 additions & 3 deletions src/components/NoteManager/components/NoteLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CodeSyncContext, type ICodeSyncContext } from "../../CodeSyncProvider/C
import { type ILocationContext, LocationContext } from "../../LocationProvider/LocationProvider";
import type { INotesContext } from "../../NotesProvider/NotesProvider";
import { type IDecoratedNote, NoteSource } from "../../NotesProvider/types/DecoratedNote";
import { OutlineLink } from "../../Outline";
import { type ISelectionContext, SelectionContext } from "../../SelectionProvider/SelectionProvider";

type NoteLinkProps = {
Expand Down Expand Up @@ -105,9 +106,13 @@ export function NoteLink({ note, onEditNote }: NoteLinkProps) {
</a>
)}

<a href="#" onClick={handleNoteTitleClick} className="default-link">
p. {pageNumber} &gt; {section} {subSection ? `> ${subSection}` : null}
</a>
<OutlineLink
firstLevel
title={`${section} ${subSection ? `${subSection} ` : ""}`}
number={`p. ${pageNumber} >`}
onClick={handleNoteTitleClick}
href="#"
/>

{migrationFlag && isEditable && (
<a
Expand Down
Loading