fix: Bookmarklet improvements and header layout fixes
- Fix bookmarklet URL to use absolute path with window.location.origin - Change capture prefix from 'rec:' to 'web:' for web captures - Add BookmarkletInstructions to header and preferences panel - Redesign QuickAdd as dropdown popup (no header overflow) - Move capture button and work mode to mobile menu - Fix isOpen bug in BookmarkletInstructions dialog
This commit is contained in:
@@ -5,8 +5,9 @@ import { useRouter } from 'next/navigation'
|
||||
import { toast } from 'sonner'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Plus, Loader2, Text, Sparkles, X } from 'lucide-react'
|
||||
import { Plus, Loader2, Sparkles, X, Text, ChevronDown } from 'lucide-react'
|
||||
import { inferNoteType, formatContentForType } from '@/lib/type-inference'
|
||||
import { NoteType } from '@/types/note'
|
||||
|
||||
@@ -30,12 +31,13 @@ const TYPE_LABELS: Record<NoteType, string> = {
|
||||
export function QuickAdd() {
|
||||
const [value, setValue] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isMultiline, setIsMultiline] = useState(false)
|
||||
const [typeSuggestion, setTypeSuggestion] = useState<TypeSuggestion | null>(null)
|
||||
const [dismissedSuggestion, setDismissedSuggestion] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const popupRef = useRef<HTMLDivElement>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const detectContentType = useCallback((text: string) => {
|
||||
@@ -58,7 +60,6 @@ export function QuickAdd() {
|
||||
}, [dismissedSuggestion])
|
||||
|
||||
const handlePaste = (e: React.ClipboardEvent) => {
|
||||
// Let the paste happen first
|
||||
setDismissedSuggestion(false)
|
||||
setTimeout(() => {
|
||||
detectContentType(value)
|
||||
@@ -70,7 +71,7 @@ export function QuickAdd() {
|
||||
setValue(typeSuggestion.formattedContent)
|
||||
setTypeSuggestion(null)
|
||||
setIsMultiline(true)
|
||||
setIsExpanded(true)
|
||||
setIsOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +102,8 @@ export function QuickAdd() {
|
||||
description: note.title,
|
||||
})
|
||||
setValue('')
|
||||
setIsExpanded(false)
|
||||
setIsOpen(false)
|
||||
setIsMultiline(false)
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
toast.error('Error', {
|
||||
@@ -119,7 +121,7 @@ export function QuickAdd() {
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setValue('')
|
||||
setIsExpanded(false)
|
||||
setIsOpen(false)
|
||||
setIsMultiline(false)
|
||||
inputRef.current?.blur()
|
||||
textareaRef.current?.blur()
|
||||
@@ -129,25 +131,39 @@ export function QuickAdd() {
|
||||
const toggleMultiline = () => {
|
||||
setIsMultiline(!isMultiline)
|
||||
if (!isMultiline) {
|
||||
setIsExpanded(true)
|
||||
setTimeout(() => textareaRef.current?.focus(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputFocus = () => {
|
||||
setIsOpen(true)
|
||||
}
|
||||
|
||||
// Close popup when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (popupRef.current && !popupRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isOpen])
|
||||
|
||||
// Focus on keyboard shortcut
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||
// Ctrl+N or Cmd+N to focus quick add
|
||||
if ((e.key === 'n' && (e.metaKey || e.ctrlKey)) || (e.key === 'n' && e.altKey)) {
|
||||
e.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.select()
|
||||
setIsExpanded(true)
|
||||
setIsOpen(true)
|
||||
}
|
||||
// Escape to blur
|
||||
if (e.key === 'Escape' && document.activeElement === inputRef.current) {
|
||||
inputRef.current?.blur()
|
||||
setIsExpanded(false)
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleGlobalKeyDown)
|
||||
@@ -155,115 +171,128 @@ export function QuickAdd() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<form onSubmit={handleSubmit} className="flex items-end gap-2">
|
||||
<div className="relative flex-1">
|
||||
{isMultiline ? (
|
||||
<div className="relative" ref={popupRef}>
|
||||
{/* Compact input row */}
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-1.5">
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="cmd: título..."
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
detectContentType(e.target.value)
|
||||
if (e.target.value) setIsOpen(true)
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleInputFocus}
|
||||
onPaste={handlePaste}
|
||||
className="w-full sm:w-80 h-9 pr-16"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{isLoading && (
|
||||
<Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
{/* Action buttons inside input */}
|
||||
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMultiline}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-accent transition-colors',
|
||||
isMultiline && 'bg-accent text-accent-foreground'
|
||||
)}
|
||||
title={isMultiline ? 'Modo línea' : 'Modo multilínea'}
|
||||
>
|
||||
{isMultiline ? (
|
||||
<ChevronDown className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Text className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!value.trim() || isLoading}
|
||||
className={cn(
|
||||
'p-1 rounded hover:bg-accent transition-colors',
|
||||
'disabled:pointer-events-none disabled:opacity-30'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Expanded popup */}
|
||||
{isOpen && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 p-3 bg-popover border rounded-lg shadow-lg z-50">
|
||||
{/* Multiline textarea (shown when multiline mode) */}
|
||||
{isMultiline && (
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
placeholder="cmd: título #tag Contenido multilínea..."
|
||||
placeholder="Contenido multilínea..."
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value)
|
||||
detectContentType(e.target.value)
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsExpanded(true)}
|
||||
onPaste={handlePaste}
|
||||
className={cn(
|
||||
'min-h-[80px] max-h-[200px] transition-all duration-200 resize-none',
|
||||
isExpanded && 'w-full'
|
||||
)}
|
||||
disabled={isLoading}
|
||||
rows={isExpanded ? 4 : 2}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder="cmd: título #tag..."
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsExpanded(true)}
|
||||
onPaste={handlePaste}
|
||||
className={cn(
|
||||
'w-24 xs:w-32 sm:w-48 transition-all duration-200',
|
||||
isExpanded && 'w-40 xs:w-48 sm:w-72'
|
||||
)}
|
||||
className="min-h-[100px] max-h-[200px] resize-none w-full"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleMultiline}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-lg border bg-background p-1.5 sm:p-2',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'transition-colors',
|
||||
isMultiline && 'bg-accent text-accent-foreground'
|
||||
)}
|
||||
title={isMultiline ? 'Modo línea' : 'Modo multilínea'}
|
||||
>
|
||||
<Text className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!value.trim() || isLoading}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-lg border bg-background p-1.5 sm:p-2',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-3 w-3 sm:h-4 sm:w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Smart paste suggestion */}
|
||||
{typeSuggestion && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 p-3 bg-popover border rounded-lg shadow-md z-50">
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles className="h-4 w-4 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">
|
||||
Detectado: <span className="text-primary">{TYPE_LABELS[typeSuggestion.type]}</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{typeSuggestion.reason}</p>
|
||||
{/* Smart paste suggestion */}
|
||||
{typeSuggestion && (
|
||||
<div className="mt-2 p-2 bg-muted/50 rounded-lg border">
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles className="h-4 w-4 text-primary mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium">
|
||||
Detectado: <span className="text-primary">{TYPE_LABELS[typeSuggestion.type]}</span>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{typeSuggestion.reason}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissSuggestion}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={acceptSuggestion}
|
||||
className="text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90"
|
||||
>
|
||||
Usar plantilla
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissSuggestion}
|
||||
className="text-xs px-2 py-1 text-muted-foreground hover:bg-accent rounded"
|
||||
>
|
||||
Descartar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissSuggestion}
|
||||
className="p-1 hover:bg-accent rounded"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={acceptSuggestion}
|
||||
className="text-xs px-2 py-1 bg-primary text-primary-foreground rounded hover:bg-primary/90"
|
||||
>
|
||||
Usar plantilla
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={dismissSuggestion}
|
||||
className="text-xs px-2 py-1 text-muted-foreground hover:bg-accent rounded"
|
||||
>
|
||||
Descartar
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Help text */}
|
||||
{!value && !typeSuggestion && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Usa prefijos como <span className="font-mono bg-muted px-1 rounded">cmd:</span>, <span className="font-mono bg-muted px-1 rounded">snip:</span> para тип notes
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user