1- import React , { useState , useEffect , useRef } from "react" ;
1+ import React , { useState , useEffect , useRef , useMemo } from "react" ;
22import { AnnotationType } from "../types" ;
33import { createPortal } from "react-dom" ;
44import { useDismissOnOutsideAndEscape } from "../hooks/useDismissOnOutsideAndEscape" ;
5+ import { type QuickLabel , getQuickLabels , getLabelColors } from "../utils/quickLabels" ;
56
67type PositionMode = 'center-above' | 'top-right' ;
78
@@ -19,6 +20,8 @@ interface AnnotationToolbarProps {
1920 onClose : ( ) => void ;
2021 /** Called when user wants to write a comment (opens CommentPopover in parent) */
2122 onRequestComment ?: ( initialChar ?: string ) => void ;
23+ /** Called when a quick label chip is selected */
24+ onQuickLabel ?: ( label : QuickLabel ) => void ;
2225 /** Text to copy (for text selection, pass source.text) */
2326 copyText ?: string ;
2427 /** Close toolbar when element scrolls out of viewport */
@@ -36,6 +39,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
3639 onAnnotate,
3740 onClose,
3841 onRequestComment,
42+ onQuickLabel,
3943 copyText,
4044 closeOnScrollOut = false ,
4145 isExiting = false ,
@@ -44,7 +48,9 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
4448} ) => {
4549 const [ position , setPosition ] = useState < { top : number ; left ?: number ; right ?: number } | null > ( null ) ;
4650 const [ copied , setCopied ] = useState ( false ) ;
51+ const [ showQuickLabels , setShowQuickLabels ] = useState ( false ) ;
4752 const toolbarRef = useRef < HTMLDivElement > ( null ) ;
53+ const quickLabels = useMemo ( ( ) => getQuickLabels ( ) , [ ] ) ;
4854
4955 const handleCopy = async ( ) => {
5056 let textToCopy = copyText ;
@@ -95,12 +101,27 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
95101 } ;
96102 } , [ element , positionMode , closeOnScrollOut , onClose ] ) ;
97103
98- // Type-to-comment: typing opens CommentPopover via parent
104+ // Type-to-comment + Alt+N quick label shortcuts
99105 useEffect ( ( ) => {
100106 const handleKeyDown = ( e : KeyboardEvent ) => {
101107 if ( e . isComposing ) return ;
102108 if ( isEditableElement ( e . target ) || isEditableElement ( document . activeElement ) ) return ;
103- if ( e . key === "Escape" ) { onClose ( ) ; return ; }
109+ if ( e . key === "Escape" ) {
110+ setShowQuickLabels ( false ) ;
111+ onClose ( ) ;
112+ return ;
113+ }
114+
115+ // Alt+1..8: apply quick label
116+ if ( e . altKey && e . code >= 'Digit1' && e . code <= 'Digit8' ) {
117+ e . preventDefault ( ) ;
118+ const index = parseInt ( e . code . slice ( 5 ) , 10 ) - 1 ;
119+ if ( index < quickLabels . length ) {
120+ onQuickLabel ?.( quickLabels [ index ] ) ;
121+ }
122+ return ;
123+ }
124+
104125 if ( e . ctrlKey || e . metaKey || e . altKey ) return ;
105126 if ( e . key === "Tab" || e . key === "Enter" ) return ;
106127 if ( e . key . length !== 1 ) return ;
@@ -110,7 +131,7 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
110131
111132 window . addEventListener ( "keydown" , handleKeyDown ) ;
112133 return ( ) => window . removeEventListener ( "keydown" , handleKeyDown ) ;
113- } , [ onClose , onRequestComment ] ) ;
134+ } , [ onClose , onRequestComment , onQuickLabel , quickLabels ] ) ;
114135
115136 useDismissOnOutsideAndEscape ( {
116137 enabled : true ,
@@ -180,6 +201,25 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
180201 label = "Comment"
181202 className = "text-accent hover:bg-accent/10"
182203 />
204+ { onQuickLabel && (
205+ < div className = "relative" >
206+ < ToolbarButton
207+ onClick = { ( ) => setShowQuickLabels ( prev => ! prev ) }
208+ icon = { < ZapIcon /> }
209+ label = "Quick label"
210+ className = { showQuickLabels ? "text-amber-500 bg-amber-500/10" : "text-amber-500 hover:bg-amber-500/10" }
211+ />
212+ { showQuickLabels && (
213+ < QuickLabelDropdown
214+ labels = { quickLabels }
215+ onSelect = { ( label ) => {
216+ setShowQuickLabels ( false ) ;
217+ onQuickLabel ( label ) ;
218+ } }
219+ />
220+ ) }
221+ </ div >
222+ ) }
183223 < div className = "w-px h-5 bg-border mx-0.5" />
184224 < ToolbarButton
185225 onClick = { onClose }
@@ -193,6 +233,45 @@ export const AnnotationToolbar: React.FC<AnnotationToolbarProps> = ({
193233 ) ;
194234} ;
195235
236+ // Quick Label Dropdown
237+ const QuickLabelDropdown : React . FC < {
238+ labels : QuickLabel [ ] ;
239+ onSelect : ( label : QuickLabel ) => void ;
240+ } > = ( { labels, onSelect } ) => {
241+ const isMac = navigator . platform ?. includes ( 'Mac' ) ;
242+ const altKey = isMac ? '⌥' : 'Alt+' ;
243+
244+ return (
245+ < div
246+ className = "absolute top-full left-1/2 -translate-x-1/2 mt-1.5 bg-popover border border-border rounded-lg shadow-2xl p-2 min-w-[220px] z-[101]"
247+ style = { { animation : 'annotation-toolbar-in 0.1s ease-out' } }
248+ onMouseDown = { ( e ) => e . stopPropagation ( ) }
249+ >
250+ < div className = "text-[10px] text-muted-foreground/60 px-1 mb-1.5 font-medium uppercase tracking-wide" > Quick Labels</ div >
251+ < div className = "flex flex-wrap gap-1" >
252+ { labels . map ( ( label , index ) => {
253+ const colors = getLabelColors ( label . color ) ;
254+ return (
255+ < button
256+ key = { label . id }
257+ onClick = { ( ) => onSelect ( label ) }
258+ className = "inline-flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium transition-opacity hover:opacity-75 active:opacity-60"
259+ style = { { backgroundColor : colors . bg , color : colors . text } }
260+ title = { index < 8 ? `${ altKey } ${ index + 1 } ` : undefined }
261+ >
262+ < span > { label . emoji } </ span >
263+ < span > { label . text } </ span >
264+ { index < 8 && (
265+ < span className = "text-[9px] opacity-40 ml-0.5" > { index + 1 } </ span >
266+ ) }
267+ </ button >
268+ ) ;
269+ } ) }
270+ </ div >
271+ </ div >
272+ ) ;
273+ } ;
274+
196275// Icons
197276const CopyIcon = ( ) => (
198277 < svg className = "w-4 h-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
@@ -218,6 +297,12 @@ const CommentIcon = () => (
218297 </ svg >
219298) ;
220299
300+ const ZapIcon = ( ) => (
301+ < svg className = "w-4 h-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
302+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M13 10V3L4 14h7v7l9-11h-7z" />
303+ </ svg >
304+ ) ;
305+
221306const CloseIcon = ( ) => (
222307 < svg className = "w-4 h-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
223308 < path strokeLinecap = "round" strokeLinejoin = "round" d = "M6 18L18 6M6 6l12 12" />
0 commit comments