this repo has no description
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: scrollArea for suggestion buttons

+161 -88
+161 -88
mast-react-vite/src/components/ui/action-parser.tsx
··· 4 4 import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; 5 5 import { Search, Plus, Pencil, Check, Calendar } from "lucide-react"; 6 6 7 - // Add onActionChange to InputProps interface 8 7 export interface InputProps 9 8 extends React.InputHTMLAttributes<HTMLInputElement> { 10 9 onActionChange?: (action: string) => void; 11 - onSubmit?: () => void; // Add this prop 10 + onSubmit?: () => void; 12 11 ctx?: any; 13 12 } 14 13 ··· 18 17 const [selectedToggle, setSelectedToggle] = React.useState("add"); 19 18 const options = ["add", "filter", "done", "delete"]; 20 19 const [suggestions, setSuggestions] = React.useState<string[]>([]); 20 + const [showShadow, setShowShadow] = React.useState(false); 21 21 22 - // Create internal ref for focusing 23 - const inputRef = React.useRef<HTMLInputElement>(null); 22 + // Create refs for the suggestion containers and inputs 23 + const suggestionsRef = React.useRef<HTMLDivElement>(null); 24 + const mobileSuggestionsRef = React.useRef<HTMLDivElement>(null); 25 + const desktopInputRef = React.useRef<HTMLInputElement>(null); 26 + const mobileInputRef = React.useRef<HTMLInputElement>(null); 24 27 25 - // Helper function to focus the input 26 28 const focusInput = () => { 27 - if (inputRef.current) { 28 - inputRef.current.focus(); 29 + if (desktopInputRef.current) { 30 + try { 31 + desktopInputRef.current.focus(); 32 + } catch (e) { 33 + console.error('Error focusing desktop input:', e); 34 + } 35 + } 36 + 37 + if (mobileInputRef.current) { 38 + try { 39 + mobileInputRef.current.focus(); 40 + } catch (e) { 41 + console.error('Error focusing mobile input:', e); 42 + } 29 43 } 30 44 }; 31 45 ··· 42 56 onSubmit(); 43 57 } 44 58 }; 59 + 60 + const checkScrollShadow = (element: HTMLDivElement | null) => { 61 + if (!element) return; 62 + const hasMoreContent = element.scrollWidth > element.clientWidth; 63 + 64 + // Check if we're at the end of scrolling 65 + const isAtEnd = Math.abs(element.scrollWidth - element.clientWidth - element.scrollLeft) < 1; 66 + setShowShadow(hasMoreContent && !isAtEnd); 67 + }; 68 + const handleSuggestionsScroll = (event: React.UIEvent<HTMLDivElement>) => { 69 + checkScrollShadow(event.currentTarget); 70 + }; 45 71 46 72 React.useEffect(() => { 47 73 if (!ctx) return; ··· 55 81 const fetchSuggestions = async () => { 56 82 try { 57 83 if (lastWord.startsWith('#')) { 58 - // Extract the partial tag (text after the #) 59 84 const partialTag = lastWord.substring(1); 60 85 61 86 // Query for tags that match the partial input 62 87 const result = await ctx.db.exec( 63 88 `SELECT DISTINCT value as tag FROM todos, json_each(todos.tags) 64 - WHERE completed = 0 AND value LIKE ? LIMIT 5`, 65 - [`${partialTag}%`] // Use parameter to prevent SQL injection 89 + WHERE completed = 0 AND value LIKE ?`, 90 + [`${partialTag}%`] 66 91 ); 67 92 68 - // Process the results 69 93 let tags: string[] = []; 70 94 if (result && Array.isArray(result)) { 71 95 if (result.length > 0) { ··· 86 110 // Query for projects that match the partial input 87 111 const result = await ctx.db.exec( 88 112 `SELECT DISTINCT project FROM todos 89 - WHERE project != '' AND completed = 0 AND project LIKE ? LIMIT 5`, 113 + WHERE project != '' AND completed = 0 AND project LIKE ?`, 90 114 [`${partialProject}%`] 91 115 ); 92 116 ··· 116 140 117 141 }, [value, ctx]); 118 142 143 + // Check for shadow when suggestions change 144 + React.useEffect(() => { 145 + // Allow the DOM to update before checking shadow 146 + const timer = setTimeout(() => { 147 + checkScrollShadow(suggestionsRef.current); 148 + checkScrollShadow(mobileSuggestionsRef.current); 149 + }, 0); 150 + 151 + return () => clearTimeout(timer); 152 + }, [suggestions]); 153 + 154 + // Handle forwarded ref 155 + React.useEffect(() => { 156 + if (ref) { 157 + // Forward the ref to point to both input refs 158 + if (typeof ref === 'function') { 159 + // Function ref case 160 + ref(desktopInputRef.current || mobileInputRef.current); 161 + } else { 162 + // Object ref case 163 + ref.current = desktopInputRef.current || mobileInputRef.current; 164 + } 165 + } 166 + }, [ref]); 167 + 119 168 return ( 120 169 <div className="flex flex-col group"> 121 170 <div className="hidden md:flex flex-col w-full"> ··· 171 220 "focus:outline-none", 172 221 className, 173 222 )} 174 - ref={inputRef} 223 + ref={desktopInputRef} 175 224 value={value} 176 225 {...props} 177 226 /> ··· 182 231 183 232 {/* Second line (non-interactive) with submit button */} 184 233 <div className="flex-1 min-h-[36px] border border-t-0 rounded-r-none rounded-bl-md rounded-br-md bg-background px-3 py-2 text-sm text-muted-foreground flex items-center justify-between group-focus-within:border-ring"> 185 - <div className="flex space-x-3"> 234 + <div className="flex space-x-3 items-center flex-grow overflow-hidden"> 186 235 <Button 187 236 variant="ghost" 188 237 size="sm" 189 238 onClick={() => { 190 - const newValue = `${value.trim() + " " || ''}#`; 239 + const trimmedVal = value && (value as string).length > 0 ? (value as string).trim() + " " : ""; 240 + const newValue = `${trimmedVal}#`; 191 241 props.onChange?.({ target: { value: newValue } } as any); 192 - focusInput(); 242 + setTimeout(() => focusInput(), 0); 193 243 }} 194 - className="h-8 px-2 text-muted-foreground hover:text-foreground" 244 + className="h-8 px-2 text-muted-foreground hover:text-foreground flex-shrink-0" 195 245 > 196 246 # 197 247 </Button> ··· 199 249 variant="ghost" 200 250 size="sm" 201 251 onClick={() => { 202 - const newValue = `${value.trim() + " " || ''}+`; 252 + const trimmedVal = value && (value as string).length > 0 ? (value as string).trim() + " " : ""; 253 + const newValue = `${trimmedVal}+`; 203 254 props.onChange?.({ target: { value: newValue } } as any); 204 - focusInput(); 255 + setTimeout(() => focusInput(), 0); 205 256 }} 206 - className="h-8 px-2 text-muted-foreground hover:text-foreground" 257 + className="h-8 px-2 text-muted-foreground hover:text-foreground flex-shrink-0" 207 258 > 208 259 + 209 260 </Button> ··· 217 268 // props.onChange?.({ target: { value: newValue } } as any); 218 269 // } 219 270 //}} 220 - className="h-8 px-2 text-muted-foreground hover:text-foreground" 271 + className="h-8 px-2 text-muted-foreground hover:text-foreground flex-shrink-0" 221 272 > 222 273 <Calendar /> 223 274 </Button> 224 - {suggestions.length > 0 && ( 225 - <div className="flex space-x-2 overflow-x-auto"> 226 - {suggestions.map((suggestion, index) => ( 227 - <Button 228 - key={index} 229 - variant="ghost" 230 - size="sm" 231 - onClick={() => { 232 - const trimmedValue = (value as string || '').trim(); 233 - const words = trimmedValue.split(/\s+/); 234 - const lastWord = words[words.length - 1] || ''; 235 - 236 - // Replace just the last word with the suggestion 237 - if (lastWord.startsWith('#')) { 238 - words[words.length - 1] = `#${suggestion}`; 239 - } else if (lastWord.startsWith('+')) { 240 - words[words.length - 1] = `+${suggestion}`; 241 - } 242 - 243 - const newValue = words.join(' '); 244 - props.onChange?.({ target: { value: newValue } } as any); 245 - focusInput(); 246 - }} 247 - className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 275 + <div className="h-7 flex-1 min-w-0 relative"> 276 + {suggestions.length > 0 && ( 277 + <> 278 + {showShadow && ( 279 + <div className="absolute right-0 top-0 bottom-0 w-8 pointer-events-none bg-gradient-to-r from-transparent to-background z-10 shadow-sm" /> 280 + )} 281 + <div 282 + className="flex space-x-2 overflow-x-auto w-full" 283 + ref={suggestionsRef} 284 + onScroll={handleSuggestionsScroll} 248 285 > 249 - {suggestion} 250 - </Button> 251 - ))} 252 - </div> 253 - )} 286 + {suggestions.map((suggestion, index) => ( 287 + <Button 288 + key={index} 289 + variant="ghost" 290 + size="sm" 291 + onClick={() => { 292 + const trimmedValue = (value as string || '').trim(); 293 + const words = trimmedValue.split(/\s+/); 294 + const lastWord = words[words.length - 1] || ''; 295 + 296 + // Replace just the last word with the suggestion 297 + if (lastWord.startsWith('#')) { 298 + words[words.length - 1] = `#${suggestion}`; 299 + } else if (lastWord.startsWith('+')) { 300 + words[words.length - 1] = `+${suggestion}`; 301 + } 302 + 303 + const newValue = words.join(' '); 304 + props.onChange?.({ target: { value: newValue } } as any); 305 + setTimeout(() => focusInput(), 0) 306 + }} 307 + className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 308 + > 309 + {suggestion} 310 + </Button> 311 + ))} 312 + </div> 313 + </> 314 + )}</div> 254 315 255 316 </div> 256 317 <Button ··· 325 386 "focus:outline-none", 326 387 className, 327 388 )} 328 - ref={ inputRef } 389 + ref={mobileInputRef} 329 390 value={value} 330 391 {...props} 331 392 /> ··· 336 397 337 398 {/* Second line (non-interactive) with submit button */} 338 399 <div className="flex-1 min-h-[36px] border border-t-0 rounded-b-md bg-background px-3 py-2 text-sm text-muted-foreground flex items-center justify-between group-focus-within:border-ring"> 339 - <div className="flex space-x-3"> 400 + <div className="flex space-x-3 items-center flex-grow overflow-hidden"> 340 401 <Button 341 402 variant="ghost" 342 403 size="sm" 343 404 onClick={() => { 344 - const newValue = `${value.trim() + " " || ''}#`; 405 + const trimmedVal = value && (value as string).length > 0 ? (value as string).trim() + " " : ""; 406 + const newValue = `${trimmedVal}#`; 345 407 props.onChange?.({ target: { value: newValue } } as any); 346 - focusInput(); 408 + setTimeout(() => focusInput(), 0) 347 409 }} 348 - className="h-8 px-2 text-muted-foreground hover:text-foreground" 410 + className="h-8 px-2 text-muted-foreground hover:text-foreground flex-shrink-0" 349 411 > 350 412 # 351 413 </Button> ··· 353 415 variant="ghost" 354 416 size="sm" 355 417 onClick={() => { 356 - const newValue = `${value.trim() + " " || ''}+`; 418 + const trimmedVal = value && (value as string).length > 0 ? (value as string).trim() + " " : ""; 419 + const newValue = `${trimmedVal}+`; 357 420 props.onChange?.({ target: { value: newValue } } as any); 358 - focusInput(); 421 + setTimeout(() => focusInput(), 0) 359 422 }} 360 - className="h-8 px-2 text-muted-foreground hover:text-foreground" 423 + className="h-8 px-2 text-muted-foreground hover:text-foreground flex-shrink-0" 361 424 > 362 425 + 363 426 </Button> ··· 371 434 // props.onChange?.({ target: { value: newValue } } as any); 372 435 // } 373 436 //}} 374 - className="h-8 px-2 text-muted-foreground hover:text-foreground" 437 + className="h-8 px-2 text-muted-foreground hover:text-foreground flex-shrink-0" 375 438 > 376 439 <Calendar /> 377 440 </Button> 378 - {suggestions.length > 0 && ( 379 - <div className="flex space-x-2 overflow-x-auto"> 380 - {suggestions.map((suggestion, index) => ( 381 - <Button 382 - key={index} 383 - variant="ghost" 384 - size="sm" 385 - onClick={() => { 386 - const trimmedValue = (value as string || '').trim(); 387 - const words = trimmedValue.split(/\s+/); 388 - const lastWord = words[words.length - 1] || ''; 389 - 390 - // Replace just the last word with the suggestion 391 - if (lastWord.startsWith('#')) { 392 - words[words.length - 1] = `#${suggestion}`; 393 - } else if (lastWord.startsWith('+')) { 394 - words[words.length - 1] = `+${suggestion}`; 395 - } 396 - 397 - const newValue = words.join(' '); 398 - props.onChange?.({ target: { value: newValue } } as any); 399 - focusInput(); 400 - }} 401 - className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 441 + <div className="h-7 flex-1 min-w-0 relative"> 442 + {suggestions.length > 0 && ( 443 + <> 444 + {showShadow && ( 445 + <div className="absolute right-0 top-0 bottom-0 w-8 pointer-events-none bg-gradient-to-r from-transparent to-background z-10 shadow-sm" /> 446 + )} 447 + <div 448 + className="flex space-x-2 overflow-x-auto w-full" 449 + ref={mobileSuggestionsRef} 450 + onScroll={handleSuggestionsScroll} 402 451 > 403 - {suggestion} 404 - </Button> 405 - ))} 406 - </div> 407 - )} 452 + {suggestions.map((suggestion, index) => ( 453 + <Button 454 + key={index} 455 + variant="ghost" 456 + size="sm" 457 + onClick={() => { 458 + const trimmedValue = (value as string || '').trim(); 459 + const words = trimmedValue.split(/\s+/); 460 + const lastWord = words[words.length - 1] || ''; 461 + 462 + // Replace just the last word with the suggestion 463 + if (lastWord.startsWith('#')) { 464 + words[words.length - 1] = `#${suggestion}`; 465 + } else if (lastWord.startsWith('+')) { 466 + words[words.length - 1] = `+${suggestion}`; 467 + } 468 + 469 + const newValue = words.join(' '); 470 + props.onChange?.({ target: { value: newValue } } as any); 471 + setTimeout(() => focusInput(), 0); 472 + }} 473 + className="h-7 px-2 text-muted-foreground hover:text-foreground whitespace-nowrap" 474 + > 475 + {suggestion} 476 + </Button> 477 + ))} 478 + </div> 479 + </> 480 + )}</div> 408 481 409 482 </div> 410 483 <Button