|
9 | 9 | import * as React from 'react'; |
10 | 10 | import { cn, Button, Input, Popover, PopoverContent, PopoverTrigger, FilterBuilder, SortBuilder, NavigationOverlay } from '@object-ui/components'; |
11 | 11 | import type { SortItem } from '@object-ui/components'; |
12 | | -import { Search, SlidersHorizontal, ArrowUpDown, X } from 'lucide-react'; |
| 12 | +import { Search, SlidersHorizontal, ArrowUpDown, X, EyeOff, Group, Paintbrush, Ruler } from 'lucide-react'; |
13 | 13 | import type { FilterGroup } from '@object-ui/components'; |
14 | 14 | import { ViewSwitcher, ViewType } from './ViewSwitcher'; |
15 | 15 | import { SchemaRenderer, useNavigationOverlay } from '@object-ui/react'; |
@@ -390,126 +390,181 @@ export const ListView: React.FC<ListViewProps> = ({ |
390 | 390 | })); |
391 | 391 | }, [objectDef, schema.fields]); |
392 | 392 |
|
| 393 | + const [searchExpanded, setSearchExpanded] = React.useState(false); |
| 394 | + |
393 | 395 | return ( |
394 | 396 | <div className={cn('flex flex-col h-full bg-background', className)}> |
395 | | - {/* Airtable-style Toolbar */} |
396 | | - <div className="border-b px-4 py-2 flex items-center justify-between gap-4 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> |
397 | | - <div className="flex items-center gap-2 flex-1 overflow-hidden"> |
398 | | - {/* View Switcher on the Left (optional, hidden by default) */} |
399 | | - {showViewSwitcher && ( |
400 | | - <div className="flex items-center pr-2 border-r mr-2"> |
401 | | - <ViewSwitcher |
402 | | - currentView={currentView} |
403 | | - availableViews={availableViews} |
404 | | - onViewChange={handleViewChange} |
405 | | - /> |
406 | | - </div> |
407 | | - )} |
408 | | - |
409 | | - {/* Action Tools */} |
410 | | - <div className="flex items-center gap-1"> |
411 | | - <Popover open={showFilters} onOpenChange={setShowFilters}> |
412 | | - <PopoverTrigger asChild> |
413 | | - <Button |
414 | | - variant={hasFilters ? "secondary" : "ghost"} |
415 | | - size="sm" |
416 | | - className={cn( |
417 | | - "h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary", |
418 | | - hasFilters && "text-primary bg-secondary/50" |
419 | | - )} |
420 | | - > |
421 | | - <SlidersHorizontal className="h-4 w-4 mr-2" /> |
422 | | - <span className="hidden lg:inline">Filter</span> |
423 | | - {hasFilters && ( |
424 | | - <span className="ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"> |
425 | | - {currentFilters.conditions?.length || 0} |
426 | | - </span> |
427 | | - )} |
428 | | - </Button> |
429 | | - </PopoverTrigger> |
430 | | - <PopoverContent align="start" className="w-[600px] p-4"> |
431 | | - <div className="space-y-4"> |
432 | | - <div className="flex items-center justify-between border-b pb-2"> |
433 | | - <h4 className="font-medium text-sm">Filter Records</h4> |
434 | | - </div> |
435 | | - <FilterBuilder |
436 | | - fields={filterFields} |
437 | | - value={currentFilters} |
438 | | - onChange={(newFilters) => { |
439 | | - console.log('Filter Changed:', newFilters); |
440 | | - setCurrentFilters(newFilters); |
441 | | - // Convert FilterBuilder format to OData $filter string if needed |
442 | | - // For now we just update state and notify listener |
443 | | - // In a real app, this would likely build an OData string |
444 | | - if (onFilterChange) onFilterChange(newFilters); |
445 | | - }} |
446 | | - /> |
447 | | - </div> |
448 | | - </PopoverContent> |
449 | | - </Popover> |
450 | | - |
451 | | - <Popover open={showSort} onOpenChange={setShowSort}> |
452 | | - <PopoverTrigger asChild> |
453 | | - <Button |
454 | | - variant={currentSort.length > 0 ? "secondary" : "ghost"} |
455 | | - size="sm" |
456 | | - className={cn( |
457 | | - "h-8 px-2 lg:px-3 text-muted-foreground hover:text-primary", |
458 | | - currentSort.length > 0 && "text-primary bg-secondary/50" |
459 | | - )} |
460 | | - > |
461 | | - <ArrowUpDown className="h-4 w-4 mr-2" /> |
462 | | - <span className="hidden lg:inline">Sort</span> |
463 | | - {currentSort.length > 0 && ( |
464 | | - <span className="ml-1.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"> |
465 | | - {currentSort.length} |
466 | | - </span> |
467 | | - )} |
468 | | - </Button> |
469 | | - </PopoverTrigger> |
470 | | - <PopoverContent align="start" className="w-[600px] p-4"> |
471 | | - <div className="space-y-4"> |
472 | | - <div className="flex items-center justify-between border-b pb-2"> |
473 | | - <h4 className="font-medium text-sm">Sort Records</h4> |
474 | | - </div> |
475 | | - <SortBuilder |
476 | | - fields={filterFields} |
477 | | - value={currentSort} |
478 | | - onChange={(newSort) => { |
479 | | - console.log('Sort Changed:', newSort); |
480 | | - setCurrentSort(newSort); |
481 | | - if (onSortChange) onSortChange(newSort); |
482 | | - }} |
483 | | - /> |
484 | | - </div> |
485 | | - </PopoverContent> |
486 | | - </Popover> |
487 | | - |
488 | | - {/* Future: Group, Color, Height */} |
489 | | - </div> |
| 397 | + {/* Airtable-style Toolbar — Row 1: View tabs */} |
| 398 | + {showViewSwitcher && ( |
| 399 | + <div className="border-b px-4 py-1 flex items-center bg-background"> |
| 400 | + <ViewSwitcher |
| 401 | + currentView={currentView} |
| 402 | + availableViews={availableViews} |
| 403 | + onViewChange={handleViewChange} |
| 404 | + /> |
490 | 405 | </div> |
| 406 | + )} |
491 | 407 |
|
492 | | - {/* Right Actions: Search + New */} |
493 | | - <div className="flex items-center gap-2"> |
494 | | - <div className="relative w-40 lg:w-64 transition-all"> |
495 | | - <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> |
496 | | - <Input |
497 | | - placeholder="Find..." |
498 | | - value={searchTerm} |
499 | | - onChange={(e) => handleSearchChange(e.target.value)} |
500 | | - className="pl-8 h-8 text-sm bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors" |
| 408 | + {/* Airtable-style Toolbar — Row 2: Tool buttons */} |
| 409 | + <div className="border-b px-4 py-1 flex items-center justify-between gap-2 bg-background"> |
| 410 | + <div className="flex items-center gap-0.5 overflow-hidden"> |
| 411 | + {/* Hide Fields */} |
| 412 | + <Button |
| 413 | + variant="ghost" |
| 414 | + size="sm" |
| 415 | + className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" |
| 416 | + disabled |
| 417 | + > |
| 418 | + <EyeOff className="h-3.5 w-3.5 mr-1.5" /> |
| 419 | + <span className="hidden sm:inline">Hide fields</span> |
| 420 | + </Button> |
| 421 | + |
| 422 | + {/* Filter */} |
| 423 | + <Popover open={showFilters} onOpenChange={setShowFilters}> |
| 424 | + <PopoverTrigger asChild> |
| 425 | + <Button |
| 426 | + variant="ghost" |
| 427 | + size="sm" |
| 428 | + className={cn( |
| 429 | + "h-7 px-2 text-muted-foreground hover:text-primary text-xs", |
| 430 | + hasFilters && "text-primary" |
| 431 | + )} |
| 432 | + > |
| 433 | + <SlidersHorizontal className="h-3.5 w-3.5 mr-1.5" /> |
| 434 | + <span className="hidden sm:inline">Filter</span> |
| 435 | + {hasFilters && ( |
| 436 | + <span className="ml-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"> |
| 437 | + {currentFilters.conditions?.length || 0} |
| 438 | + </span> |
| 439 | + )} |
| 440 | + </Button> |
| 441 | + </PopoverTrigger> |
| 442 | + <PopoverContent align="start" className="w-[600px] p-4"> |
| 443 | + <div className="space-y-4"> |
| 444 | + <div className="flex items-center justify-between border-b pb-2"> |
| 445 | + <h4 className="font-medium text-sm">Filter Records</h4> |
| 446 | + </div> |
| 447 | + <FilterBuilder |
| 448 | + fields={filterFields} |
| 449 | + value={currentFilters} |
| 450 | + onChange={(newFilters) => { |
| 451 | + setCurrentFilters(newFilters); |
| 452 | + if (onFilterChange) onFilterChange(newFilters); |
| 453 | + }} |
501 | 454 | /> |
502 | | - {searchTerm && ( |
503 | | - <Button |
504 | | - variant="ghost" |
505 | | - size="sm" |
506 | | - className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 hover:bg-muted-foreground/20" |
507 | | - onClick={() => handleSearchChange('')} |
508 | | - > |
509 | | - <X className="h-3 w-3" /> |
510 | | - </Button> |
| 455 | + </div> |
| 456 | + </PopoverContent> |
| 457 | + </Popover> |
| 458 | + |
| 459 | + {/* Group */} |
| 460 | + <Button |
| 461 | + variant="ghost" |
| 462 | + size="sm" |
| 463 | + className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" |
| 464 | + disabled |
| 465 | + > |
| 466 | + <Group className="h-3.5 w-3.5 mr-1.5" /> |
| 467 | + <span className="hidden sm:inline">Group</span> |
| 468 | + </Button> |
| 469 | + |
| 470 | + {/* Sort */} |
| 471 | + <Popover open={showSort} onOpenChange={setShowSort}> |
| 472 | + <PopoverTrigger asChild> |
| 473 | + <Button |
| 474 | + variant="ghost" |
| 475 | + size="sm" |
| 476 | + className={cn( |
| 477 | + "h-7 px-2 text-muted-foreground hover:text-primary text-xs", |
| 478 | + currentSort.length > 0 && "text-primary" |
511 | 479 | )} |
512 | | - </div> |
| 480 | + > |
| 481 | + <ArrowUpDown className="h-3.5 w-3.5 mr-1.5" /> |
| 482 | + <span className="hidden sm:inline">Sort</span> |
| 483 | + {currentSort.length > 0 && ( |
| 484 | + <span className="ml-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary/10 text-[10px] font-medium text-primary"> |
| 485 | + {currentSort.length} |
| 486 | + </span> |
| 487 | + )} |
| 488 | + </Button> |
| 489 | + </PopoverTrigger> |
| 490 | + <PopoverContent align="start" className="w-[600px] p-4"> |
| 491 | + <div className="space-y-4"> |
| 492 | + <div className="flex items-center justify-between border-b pb-2"> |
| 493 | + <h4 className="font-medium text-sm">Sort Records</h4> |
| 494 | + </div> |
| 495 | + <SortBuilder |
| 496 | + fields={filterFields} |
| 497 | + value={currentSort} |
| 498 | + onChange={(newSort) => { |
| 499 | + setCurrentSort(newSort); |
| 500 | + if (onSortChange) onSortChange(newSort); |
| 501 | + }} |
| 502 | + /> |
| 503 | + </div> |
| 504 | + </PopoverContent> |
| 505 | + </Popover> |
| 506 | + |
| 507 | + {/* Color */} |
| 508 | + <Button |
| 509 | + variant="ghost" |
| 510 | + size="sm" |
| 511 | + className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" |
| 512 | + disabled |
| 513 | + > |
| 514 | + <Paintbrush className="h-3.5 w-3.5 mr-1.5" /> |
| 515 | + <span className="hidden sm:inline">Color</span> |
| 516 | + </Button> |
| 517 | + |
| 518 | + {/* Row Height */} |
| 519 | + <Button |
| 520 | + variant="ghost" |
| 521 | + size="sm" |
| 522 | + className="h-7 px-2 text-muted-foreground hover:text-primary text-xs hidden lg:flex" |
| 523 | + disabled |
| 524 | + > |
| 525 | + <Ruler className="h-3.5 w-3.5 mr-1.5" /> |
| 526 | + <span className="hidden sm:inline">Row height</span> |
| 527 | + </Button> |
| 528 | + </div> |
| 529 | + |
| 530 | + {/* Right: Search */} |
| 531 | + <div className="flex items-center gap-1"> |
| 532 | + {searchExpanded ? ( |
| 533 | + <div className="relative w-48 lg:w-64"> |
| 534 | + <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> |
| 535 | + <Input |
| 536 | + placeholder="Find..." |
| 537 | + value={searchTerm} |
| 538 | + onChange={(e) => handleSearchChange(e.target.value)} |
| 539 | + className="pl-7 h-7 text-xs bg-muted/50 border-transparent hover:bg-muted focus:bg-background focus:border-input transition-colors" |
| 540 | + autoFocus |
| 541 | + onBlur={() => { |
| 542 | + if (!searchTerm) setSearchExpanded(false); |
| 543 | + }} |
| 544 | + /> |
| 545 | + <Button |
| 546 | + variant="ghost" |
| 547 | + size="sm" |
| 548 | + className="absolute right-0.5 top-1/2 -translate-y-1/2 h-5 w-5 p-0 hover:bg-muted-foreground/20" |
| 549 | + onClick={() => { |
| 550 | + handleSearchChange(''); |
| 551 | + setSearchExpanded(false); |
| 552 | + }} |
| 553 | + > |
| 554 | + <X className="h-3 w-3" /> |
| 555 | + </Button> |
| 556 | + </div> |
| 557 | + ) : ( |
| 558 | + <Button |
| 559 | + variant="ghost" |
| 560 | + size="sm" |
| 561 | + className="h-7 px-2 text-muted-foreground hover:text-primary text-xs" |
| 562 | + onClick={() => setSearchExpanded(true)} |
| 563 | + > |
| 564 | + <Search className="h-3.5 w-3.5 mr-1.5" /> |
| 565 | + <span className="hidden sm:inline">Search</span> |
| 566 | + </Button> |
| 567 | + )} |
513 | 568 | </div> |
514 | 569 | </div> |
515 | 570 |
|
|
0 commit comments