Skip to content

Commit a8d528d

Browse files
authored
feat: activation note logic change
* feat: activate note when its selection matches * feat: introduce visually active note * fix: issue with wrong handling on keyup event * fix: improve accessibility and fix border styling * fix: update note button variants and reorder actions * feat: add shadow for active note * chore: udpate tests snaphosts * fix: improve usage of location context hook
1 parent dc29529 commit a8d528d

28 files changed

+342
-141
lines changed

biome.jsonc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
{
22
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
33
"files": {
4-
"ignore": ["dist/**", "graypaper-archive/**", "public/**", "tools/matrix-bot/output/messages.json"]
4+
"ignore": [
5+
"dist/**",
6+
"graypaper-archive/**",
7+
"public/**",
8+
"tools/matrix-bot/output/messages.json",
9+
"tools/snapshot-tests/playwright-report/**"
10+
]
511
},
612
"formatter": {
713
"enabled": true,

src/components/LocationProvider/LocationProvider.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ const BASE64_VALIDATION_REGEX = /^#[-A-Za-z0-9+/]*={0,3}$/;
2828

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

31+
export const useLocationContext = () => {
32+
const context = useContext(LocationContext);
33+
if (!context) {
34+
throw new Error("useLocationContext must be used within a LocationProvider");
35+
}
36+
return context;
37+
};
38+
3139
export function LocationProvider({ children }: ILocationProviderProps) {
3240
const { metadata } = useContext(MetadataContext) as IMetadataContext;
3341
const [locationParams, setLocationParams] = useState<ILocationParams>();

src/components/NoteManager/NoteManager.css

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
.note-manager{
2+
--inactive-note-bg: #ebebeb;
3+
--active-note-bg: #d8e8e7;
4+
--active-note-shadow-bg: #c8d9d8;
5+
}
6+
7+
.dark .note-manager{
8+
--inactive-note-bg: #444;
9+
--active-note-bg: #3A4949;
10+
--active-note-shadow-bg: #4A6565;
11+
12+
}
13+
114
.notes-wrapper {
215
height: 100%;
316
display: flex;
@@ -37,12 +50,7 @@
3750
border: 1px solid #f22;
3851
}
3952

40-
.note-manager blockquote {
41-
padding: 0.5rem;
42-
margin: 0.5rem 0;
43-
padding-bottom: 16px;
44-
white-space: pre-wrap;
45-
}
53+
4654

4755
.note-manager .icon {
4856
margin-right: 3px;
@@ -62,42 +70,10 @@
6270
cursor: initial;
6371
}
6472

65-
.note-manager .note {
66-
padding: 8px;
67-
border: 1px solid;
68-
border-color: light-dark(#eee, #444);
69-
margin-top: 5px;
70-
border-radius: 6px;
71-
}
72-
73-
.note-manager .note-link a {
74-
font-weight: bold;
75-
}
76-
7773
.note-manager .note .actions {
7874
display: flex;
7975
}
8076

81-
.note-manager button.remove {
82-
color: #f22;
83-
}
84-
8577
.note-manager .fill {
8678
flex: 1;
8779
}
88-
89-
.note-manager button.save {
90-
color: #2a2;
91-
}
92-
93-
.note-manager button.edit {
94-
font-size: 8px;
95-
background: none;
96-
filter: grayscale(100%);
97-
opacity: 30%;
98-
transition: all 200ms ease-in-out;
99-
}
100-
.note-manager .note:hover button.edit {
101-
filter: none;
102-
opacity: 100%;
103-
}

src/components/NoteManager/NoteManager.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function Notes() {
2727
const [noteContent, setNoteContent] = useState("");
2828
const [noteContentError, setNoteContentError] = useState("");
2929
const { locationParams } = useContext(LocationContext) as ILocationContext;
30-
const { notesReady, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
30+
const { notesReady, activeNotes, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
3131
NotesContext,
3232
) as INotesContext;
3333
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
@@ -74,7 +74,7 @@ function Notes() {
7474
}, [selectedBlocks]);
7575

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

94-
<MemoizedNotesList notes={notes} onEditNote={handleUpdateNote} onDeleteNote={handleDeleteNote} />
94+
<MemoizedNotesList
95+
activeNotes={activeNotes}
96+
notes={notes}
97+
onEditNote={handleUpdateNote}
98+
onDeleteNote={handleDeleteNote}
99+
/>
95100
</div>
96101
);
97102
}

src/components/NoteManager/components/Note.css

Whitespace-only changes.
Lines changed: 151 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
import { type ChangeEvent, type MouseEventHandler, useCallback, useState } from "react";
1+
import {
2+
type ChangeEvent,
3+
type MouseEventHandler,
4+
createContext,
5+
useCallback,
6+
useContext,
7+
useEffect,
8+
useState,
9+
} from "react";
210
import { validateMath } from "../../../utils/validateMath";
311
import { NoteContent } from "../../NoteContent/NoteContent";
412
import type { INotesContext } from "../../NotesProvider/NotesProvider";
513
import { type IDecoratedNote, NoteSource } from "../../NotesProvider/types/DecoratedNote";
614
import type { IStorageNote, UnPrefixedLabel } from "../../NotesProvider/types/StorageNote";
715
import { NoteLabels, NoteLabelsEdit } from "./NoteLabels";
816
import { NoteLink } from "./NoteLink";
17+
import "./Note.css";
18+
import { Button, cn } from "@fluffylabs/shared-ui";
19+
import { useLocationContext } from "../../LocationProvider/LocationProvider";
920

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

1526
type NoteProps = {
1627
note: IDecoratedNote;
28+
active: boolean;
1729
onEditNote: INotesContext["handleUpdateNote"];
1830
onDeleteNote: INotesContext["handleDeleteNote"];
1931
};
2032

21-
export function Note({ note, onEditNote, onDeleteNote }: NoteProps) {
33+
const noteContext = createContext<IDecoratedNote | null>(null);
34+
35+
const useNoteContext = () => {
36+
const context = useContext(noteContext);
37+
if (!context) {
38+
throw new Error("useNoteContext must be used within a NoteContextProvider");
39+
}
40+
return context;
41+
};
42+
43+
export function Note({ note, active = false, onEditNote, onDeleteNote }: NoteProps) {
2244
const [isEditing, setIsEditing] = useState(false);
2345
const [noteDirty, setNoteDirty] = useState<IStorageNote>({
2446
...note.original,
2547
});
2648
const [noteContentError, setNoteContentError] = useState("");
2749

50+
const { setLocationParams } = useLocationContext();
51+
2852
const isEditable = note.source !== NoteSource.Remote;
2953

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

97+
const handleWholeNoteClick = (e: React.MouseEvent<HTMLDivElement>) => {
98+
const target = e.target;
99+
100+
if (target instanceof Element && (target.closest("button") || target.closest("a"))) {
101+
e.preventDefault();
102+
return;
103+
}
104+
105+
if (active) {
106+
return;
107+
}
108+
109+
setLocationParams({
110+
version: note.original.version,
111+
selectionStart: note.original.selectionStart,
112+
selectionEnd: note.original.selectionEnd,
113+
});
114+
};
115+
116+
const handleNoteEnter = (e: React.KeyboardEvent<HTMLDivElement>) => {
117+
if (e.key !== "Enter" && e.key !== "Space") {
118+
e.preventDefault();
119+
}
120+
121+
if (active) {
122+
return;
123+
}
124+
125+
setLocationParams({
126+
version: note.original.version,
127+
selectionStart: note.original.selectionStart,
128+
selectionEnd: note.original.selectionEnd,
129+
});
130+
};
131+
132+
useEffect(() => {
133+
if (!active) {
134+
setIsEditing(false);
135+
}
136+
}, [active]);
137+
73138
return (
74-
<div className="note">
75-
<NoteLink note={note} onEditNote={onEditNote} />
76-
{isEditing ? (
77-
<>
78-
<textarea
79-
className={noteContentError ? "error" : ""}
80-
onChange={handleNoteContentChange}
81-
value={noteDirty.content}
82-
autoFocus
83-
/>
84-
{noteContentError ? <div className="validation-message">{noteContentError}</div> : null}
85-
</>
86-
) : (
87-
<blockquote>
88-
{note.original.author}
89-
<NoteContent content={note.original.content} />
90-
</blockquote>
91-
)}
92-
{isEditing ? <NoteLabelsEdit note={note} onNewLabels={handleEditLabels} /> : null}
93-
<div className="actions">
94-
{!isEditing ? <NoteLabels note={note} /> : null}
95-
96-
{isEditing ? (
97-
<button className="remove default-button" onClick={handleDeleteClick}>
98-
delete
99-
</button>
100-
) : null}
101-
102-
<div className="fill" />
103-
104-
{isEditable ? (
105-
<button
106-
className={`default-button ${isEditing ? "save" : "edit"}`}
107-
data-testid={isEditing ? "save-button" : "edit-button"}
108-
onClick={isEditing ? handleSaveClick : handleEditClick}
109-
>
110-
{isEditing ? "save" : "✏️"}
111-
</button>
112-
) : null}
113-
114-
{isEditing ? <button onClick={handleCancelClick}>cancel</button> : null}
139+
<NoteLayout.Root value={note}>
140+
<div
141+
data-testid="notes-manager-card"
142+
className={cn(
143+
"note rounded-xl p-4 flex flex-col gap-2",
144+
active && "bg-[var(--active-note-bg)] shadow-[0px_4px_0px_1px_var(--active-note-shadow-bg)]",
145+
!active && "bg-[var(--inactive-note-bg)] cursor-pointer",
146+
)}
147+
role={!active ? "button" : undefined}
148+
tabIndex={!active ? 0 : undefined}
149+
aria-label={!active ? "Activate label" : ""}
150+
onClick={handleWholeNoteClick}
151+
onKeyDown={handleNoteEnter}
152+
>
153+
{!active && (
154+
<>
155+
<NoteLink note={note} onEditNote={onEditNote} />
156+
<NoteLayout.Text />
157+
</>
158+
)}
159+
{active && !isEditing && (
160+
<>
161+
<div className="flex justify-between items-start">
162+
<NoteLink note={note} onEditNote={onEditNote} />
163+
{isEditable && (
164+
<Button
165+
variant="ghost"
166+
intent="neutralStrong"
167+
className="p-2 h-8"
168+
data-testid={isEditing ? "save-button" : "edit-button"}
169+
onClick={isEditing ? handleSaveClick : handleEditClick}
170+
>
171+
✏️
172+
</Button>
173+
)}
174+
</div>
175+
<NoteLayout.Text />
176+
{!isEditing ? <NoteLabels note={note} /> : null}
177+
</>
178+
)}
179+
{active && isEditing && (
180+
<>
181+
<>
182+
<NoteLink note={note} onEditNote={onEditNote} />
183+
<textarea
184+
className={noteContentError ? "error" : ""}
185+
onChange={handleNoteContentChange}
186+
value={noteDirty.content}
187+
autoFocus
188+
/>
189+
{noteContentError ? <div className="validation-message">{noteContentError}</div> : null}
190+
<NoteLabelsEdit note={note} onNewLabels={handleEditLabels} />
191+
<div className="actions gap-2">
192+
<Button variant="ghost" intent="destructive" size="sm" onClick={handleDeleteClick}>
193+
Delete
194+
</Button>
195+
<div className="fill" />
196+
<Button variant="tertiary" data-testid={"cancel-button"} onClick={handleCancelClick} size="sm">
197+
Cancel
198+
</Button>
199+
<Button data-testid={"save-button"} onClick={handleSaveClick} size="sm">
200+
Save
201+
</Button>
202+
</div>
203+
</>
204+
</>
205+
)}
115206
</div>
116-
</div>
207+
</NoteLayout.Root>
117208
);
118209
}
210+
211+
const NoteText = () => {
212+
const note = useNoteContext();
213+
214+
return (
215+
<blockquote className="whitespace-pre-wrap">
216+
{note.original.author}
217+
<NoteContent content={note.original.content} />
218+
</blockquote>
219+
);
220+
};
221+
222+
const NoteLayout = {
223+
Root: noteContext.Provider,
224+
Text: NoteText,
225+
};

src/components/NoteManager/components/NoteLink.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CodeSyncContext, type ICodeSyncContext } from "../../CodeSyncProvider/C
55
import { type ILocationContext, LocationContext } from "../../LocationProvider/LocationProvider";
66
import type { INotesContext } from "../../NotesProvider/NotesProvider";
77
import { type IDecoratedNote, NoteSource } from "../../NotesProvider/types/DecoratedNote";
8+
import { OutlineLink } from "../../Outline";
89
import { type ISelectionContext, SelectionContext } from "../../SelectionProvider/SelectionProvider";
910

1011
type NoteLinkProps = {
@@ -105,9 +106,13 @@ export function NoteLink({ note, onEditNote }: NoteLinkProps) {
105106
</a>
106107
)}
107108

108-
<a href="#" onClick={handleNoteTitleClick} className="default-link">
109-
p. {pageNumber} &gt; {section} {subSection ? `> ${subSection}` : null}
110-
</a>
109+
<OutlineLink
110+
firstLevel
111+
title={`${section} ${subSection ? `${subSection} ` : ""}`}
112+
number={`p. ${pageNumber} >`}
113+
onClick={handleNoteTitleClick}
114+
href="#"
115+
/>
111116

112117
{migrationFlag && isEditable && (
113118
<a

0 commit comments

Comments
 (0)