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" ;
210import { validateMath } from "../../../utils/validateMath" ;
311import { NoteContent } from "../../NoteContent/NoteContent" ;
412import type { INotesContext } from "../../NotesProvider/NotesProvider" ;
513import { type IDecoratedNote , NoteSource } from "../../NotesProvider/types/DecoratedNote" ;
614import type { IStorageNote , UnPrefixedLabel } from "../../NotesProvider/types/StorageNote" ;
715import { NoteLabels , NoteLabelsEdit } from "./NoteLabels" ;
816import { NoteLink } from "./NoteLink" ;
17+ import "./Note.css" ;
18+ import { Button , cn } from "@fluffylabs/shared-ui" ;
19+ import { useLocationContext } from "../../LocationProvider/LocationProvider" ;
920
1021export type NotesItem = {
1122 location : string ; // serialized InDocSelection
@@ -14,17 +25,30 @@ export type NotesItem = {
1425
1526type 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+ } ;
0 commit comments