this repo has no description

feat: animate toggleGroup position and input

Changed files
+70 -20
mast-react-vite
src
components
+70 -20
mast-react-vite/src/components/ui/action-parser.tsx
··· 19 19 const ActionParser = React.forwardRef<HTMLInputElement, InputProps>( 20 20 ({ className, ctx, type, onActionChange, onSubmit, value, ...props }, ref) => { 21 21 const [selectedIndex, setSelectedIndex] = React.useState(0); 22 - const [selectedToggle, setSelectedToggle] = React.useState("add"); 22 + const [selectedToggle, setSelectedToggle] = React.useState(""); 23 23 const options = ["add", "filter", "done", "delete"]; 24 24 const [suggestions, setSuggestions] = React.useState<string[]>([]); 25 25 const [showShadow, setShowShadow] = React.useState(false); ··· 54 54 }; 55 55 56 56 const handleToggleChange = (value: string) => { 57 - if (value) { 57 + if (value === selectedToggle) { 58 + // Clicking the active toggle should deactivate it 59 + setSelectedToggle(""); 60 + onActionChange?.(""); 61 + } else if (value) { 58 62 setSelectedToggle(value); 59 63 onActionChange?.(value); 60 64 } ··· 262 266 return ( 263 267 <div className="flex flex-col group"> 264 268 <div className="hidden md:flex flex-col w-full"> 265 - <div className="flex w-full"> 269 + <motion.div 270 + className="flex w-full" 271 + animate={{ 272 + y: 0 273 + }} 274 + transition={{ duration: 0.3, ease: "easeOut" }} 275 + > 266 276 <div className="relative w-auto"> 267 277 <ToggleGroup 268 278 type="single" 269 279 value={selectedToggle} 270 280 onValueChange={handleToggleChange} 271 - defaultValue="add" 272 281 className={cn( 273 - "h-11 border rounded-t-md border-b-0 bg-transparent text-sm cursor-default pt-1/2 pl-1 pr-1 pb-1/2", 282 + "h-11 border bg-background text-sm cursor-default pt-1/2 pl-1 pr-1 pb-1/2", 274 283 "shadow-sm transition-colors duration-0 text-center appearance-none group-focus-within:border-ring", 275 - "rounded-t-md rounded-b-none", 276 284 "focus:border-ring focus-within:border-ring", 285 + selectedToggle ? "rounded-t-md border-b-0 rounded-b-none" : "rounded-md" 277 286 )} 278 287 > 279 - <ToggleGroupItem value="filter" aria-label="filter tasks"> 288 + <ToggleGroupItem 289 + value="filter" 290 + aria-label="filter tasks" 291 + onClick={() => handleToggleChange("filter")} 292 + > 280 293 <Search className="h-4 w-4 rounded-none" /> 281 294 </ToggleGroupItem> 282 295 <ToggleGroupItem 283 296 value="modify" 284 297 aria-label="Modify selected task(s)" 298 + onClick={() => handleToggleChange("modify")} 285 299 > 286 300 <Pencil className="h-4 w-4" /> 287 301 </ToggleGroupItem> 288 - <ToggleGroupItem value="add" aria-label="Add a task"> 302 + <ToggleGroupItem 303 + value="add" 304 + aria-label="Add a task" 305 + onClick={() => handleToggleChange("add")} 306 + > 289 307 <Plus className="h-4 w-4" /> 290 308 </ToggleGroupItem> 291 309 <ToggleGroupItem 292 310 value="done" 293 311 aria-label="Toggle selected task(s) done" 312 + onClick={() => handleToggleChange("done")} 294 313 > 295 314 <Check className="h-4 w-4" /> 296 315 </ToggleGroupItem> 297 316 </ToggleGroup> 298 - <div className="absolute bottom-0 left-0 right-0 h-[2px] border group-focus-within:border-ring border-b-0 border-t-0 bg-background z-20 translate-y-[1px]"></div> 299 317 </div> 300 318 <div className="flex-1"></div> 301 - </div> 319 + </motion.div> 302 320 303 321 {/* Two-line input appearance with divider */} 304 - <div className="relative w-full flex flex-col"> 322 + <motion.div 323 + className="relative w-full flex flex-col" 324 + animate={{ 325 + opacity: selectedToggle ? 1 : 0, 326 + y: selectedToggle ? -2 : -20, 327 + height: selectedToggle ? "auto" : 0 328 + }} 329 + transition={{ duration: 0.3, ease: "easeOut" }} 330 + > 331 + {/* Connector div that bridges toggle group and input */} 332 + <div className="absolute left-0 h-[1px] border border-b-0 border-t-0 group-focus-within:border-ring bg-background z-20" style={{ width: '166px' }}></div> 333 + 305 334 {/* First line (interactive input) */} 306 335 <div className="flex-1 min-h-[36px] border border-b-0 rounded-br-none rounded-tr-md rounded-tl-none bg-background px-3 py-2 text-sm flex items-center group-focus-within:border-ring"> 307 336 <input ··· 452 481 453 482 </Button> 454 483 </div> 455 - </div> 484 + </motion.div> 456 485 </div> 457 486 458 487 {/* Mobile view layout - shown only on mobile */} 459 488 <div className="md:hidden fixed bottom-0 left-0 w-full z-50 flex flex-col items-center pb-2"> 460 - <div className="relative w-auto"> 489 + <motion.div 490 + className="relative w-auto" 491 + animate={{ 492 + y: selectedToggle ? 4 : -25 493 + }} 494 + transition={{ duration: 0.3, ease: "easeOut" }} 495 + > 461 496 <ToggleGroup 462 497 type="single" 463 498 value={selectedToggle} 464 499 onValueChange={handleToggleChange} 465 - defaultValue="add" 500 + 466 501 className={cn( 467 - "h-11 border rounded-t-md border-b-0 bg-background text-sm cursor-default pt-1/2 pl-1 pr-1 pb-1/2", 502 + "h-11 border bg-background text-sm cursor-default pt-1/2 pl-1 pr-1 pb-1/2", 468 503 "shadow-sm transition-colors duration-0 text-center appearance-none group-focus-within:border-ring", 469 - "rounded-t-md rounded-b-none", 470 504 "focus:border-ring", 505 + selectedToggle ? "rounded-t-md border-b-0 rounded-b-none" : "rounded-md" 471 506 )} 472 507 > 473 508 <ToggleGroupItem 474 509 value="filter" 475 510 aria-label="filter tasks" 476 511 className="rounded-md mx-1 p-2" 512 + onClick={() => handleToggleChange("filter")} 477 513 > 478 514 <Search className="h-5 w-5" /> 479 515 </ToggleGroupItem> ··· 481 517 value="modify" 482 518 aria-label="Modify selected task(s)" 483 519 className="rounded-md mx-1 p-2" 520 + onClick={() => handleToggleChange("modify")} 484 521 > 485 522 <Pencil className="h-5 w-5" /> 486 523 </ToggleGroupItem> ··· 488 525 value="add" 489 526 aria-label="Add a task" 490 527 className="rounded-md mx-1 p-2" 528 + onClick={() => handleToggleChange("add")} 491 529 > 492 530 <Plus className="h-5 w-5" /> 493 531 </ToggleGroupItem> ··· 495 533 value="done" 496 534 aria-label="Toggle selected task(s) done" 497 535 className="rounded-md mx-1 p-2" 536 + onClick={() => handleToggleChange("done")} 498 537 > 499 538 <Check className="h-5 w-5" /> 500 539 </ToggleGroupItem> 501 540 </ToggleGroup> 502 - <div className="absolute bottom-0 left-0 right-0 h-[2px] border border-b-0 border-t-0 group-focus-within:border-ring bg-background z-20 translate-y-[1px]" /> 503 - </div> 541 + 542 + </motion.div> 504 543 505 544 {/* Mobile two-line input appearance */} 506 - <div className="relative w-[90%] mb-1 flex flex-col"> 545 + <motion.div 546 + className="relative w-[90%] mb-1 flex flex-col" 547 + animate={{ 548 + opacity: selectedToggle ? 1 : 0, 549 + y: selectedToggle ? 0 : 20, 550 + height: selectedToggle ? "auto" : 0 551 + }} 552 + transition={{ duration: 0.3, ease: "easeOut" }} 553 + > 554 + {/* Connector div that bridges toggle group and input */} 555 + <div className="absolute -top-[1px] left-1/2 -translate-x-1/2 h-[1px] border border-b-0 border-t-0 group-focus-within:border-ring bg-background z-20 translate-y-[1px]" style={{ width: '199px' }}></div> 556 + 507 557 {/* First line (interactive input) */} 508 558 <div className="flex-1 min-h-[36px] border border-b-0 rounded-t-md bg-background px-3 py-2 text-sm flex items-center group-focus-within:border-ring"> 509 559 <input ··· 749 799 750 800 </Button> 751 801 </div> 752 - </div> 802 + </motion.div> 753 803 </div> 754 804 </div> 755 805 );