|
1 | | -import { useState, useRef, useEffect, useMemo } from "react"; |
| 1 | +import { useState, useRef, useEffect, useMemo, useCallback } from "react"; |
| 2 | +import { useIsMobile } from "@/hooks/useIsMobile"; |
2 | 3 | import * as RadixContextMenu from "@radix-ui/react-context-menu"; |
3 | 4 | import { |
4 | 5 | Cube, |
@@ -533,6 +534,7 @@ export function FeatureTree() { |
533 | 534 | const setFeatureTreeOpen = useUiStore((s) => s.setFeatureTreeOpen); |
534 | 535 | const [renamingId, setRenamingId] = useState<string | null>(null); |
535 | 536 | const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set()); |
| 537 | + const isMobile = useIsMobile(); |
536 | 538 |
|
537 | 539 | // Check if this is an assembly document |
538 | 540 | const hasInstances = document.instances && document.instances.length > 0; |
@@ -573,70 +575,90 @@ export function FeatureTree() { |
573 | 575 | } |
574 | 576 | }, [hasContent, setFeatureTreeOpen]); |
575 | 577 |
|
| 578 | + const handleBackdropClick = useCallback(() => { |
| 579 | + setFeatureTreeOpen(false); |
| 580 | + }, [setFeatureTreeOpen]); |
| 581 | + |
576 | 582 | if (!featureTreeOpen) return null; |
577 | 583 |
|
578 | 584 | return ( |
579 | | - <div |
580 | | - className={cn( |
581 | | - "absolute top-14 left-3 z-20 w-56", |
582 | | - "border border-border", |
583 | | - "bg-surface", |
584 | | - "shadow-lg shadow-black/30", |
585 | | - "max-h-[calc(100vh-120px)] flex flex-col", |
| 585 | + <> |
| 586 | + {/* Mobile backdrop */} |
| 587 | + {isMobile && ( |
| 588 | + <div |
| 589 | + className="fixed inset-0 z-10 bg-black/50 sm:hidden" |
| 590 | + onClick={handleBackdropClick} |
| 591 | + /> |
586 | 592 | )} |
587 | | - > |
588 | | - {/* Header */} |
589 | | - <div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border px-3"> |
590 | | - <span className="text-xs font-bold uppercase tracking-wider text-text-muted"> |
591 | | - Features |
592 | | - </span> |
593 | | - <button |
594 | | - onClick={() => setFeatureTreeOpen(false)} |
595 | | - className="flex h-6 w-6 items-center justify-center text-text-muted hover:text-text hover:bg-hover" |
596 | | - > |
597 | | - <X size={14} /> |
598 | | - </button> |
599 | | - </div> |
600 | 593 |
|
601 | | - {/* Body */} |
602 | | - <div className="flex-1 overflow-y-auto p-2 scrollbar-thin"> |
603 | | - {!hasContent ? ( |
604 | | - <div className="px-2 py-4 text-center text-xs text-text-muted"> |
605 | | - No features yet. |
606 | | - <br /> |
607 | | - Use the command bar to create a part. |
608 | | - </div> |
609 | | - ) : ( |
610 | | - <ContextMenu> |
611 | | - <div> |
612 | | - {/* Assembly mode: show instances and joints */} |
613 | | - {hasInstances ? ( |
614 | | - <AssemblyTree |
615 | | - instances={document.instances!} |
616 | | - joints={document.joints ?? []} |
617 | | - groundInstanceId={document.groundInstanceId} |
618 | | - /> |
619 | | - ) : ( |
620 | | - <> |
621 | | - {/* Legacy mode: show parts */} |
622 | | - {parts.map((part) => ( |
623 | | - <TreeNode |
624 | | - key={part.id} |
625 | | - part={part} |
626 | | - depth={0} |
627 | | - expandedIds={expandedIds} |
628 | | - toggleExpanded={toggleExpanded} |
629 | | - consumedParts={consumedParts} |
630 | | - renamingId={renamingId} |
631 | | - setRenamingId={setRenamingId} |
632 | | - /> |
633 | | - ))} |
634 | | - </> |
635 | | - )} |
636 | | - </div> |
637 | | - </ContextMenu> |
| 594 | + <div |
| 595 | + className={cn( |
| 596 | + // Mobile: full-height drawer from left |
| 597 | + "fixed inset-y-0 left-0 z-20 w-72", |
| 598 | + "pt-[var(--safe-top)] pb-[var(--safe-bottom)] pl-[var(--safe-left)]", |
| 599 | + // Desktop: floating panel |
| 600 | + "sm:absolute sm:top-14 sm:left-3 sm:inset-y-auto sm:w-56", |
| 601 | + "sm:pt-0 sm:pb-0 sm:pl-0", |
| 602 | + "border-r sm:border border-border", |
| 603 | + "bg-surface", |
| 604 | + "shadow-lg shadow-black/30", |
| 605 | + isMobile ? "h-full" : "max-h-[calc(100vh-120px)]", |
| 606 | + "flex flex-col", |
638 | 607 | )} |
| 608 | + > |
| 609 | + {/* Header */} |
| 610 | + <div className="flex h-10 shrink-0 items-center justify-between gap-2 border-b border-border px-3"> |
| 611 | + <span className="text-xs font-bold uppercase tracking-wider text-text-muted"> |
| 612 | + Features |
| 613 | + </span> |
| 614 | + <button |
| 615 | + onClick={() => setFeatureTreeOpen(false)} |
| 616 | + className="flex h-8 w-8 sm:h-6 sm:w-6 items-center justify-center text-text-muted hover:text-text hover:bg-hover" |
| 617 | + > |
| 618 | + <X size={14} /> |
| 619 | + </button> |
| 620 | + </div> |
| 621 | + |
| 622 | + {/* Body */} |
| 623 | + <div className="flex-1 overflow-y-auto p-2 scrollbar-thin"> |
| 624 | + {!hasContent ? ( |
| 625 | + <div className="px-2 py-4 text-center text-xs text-text-muted"> |
| 626 | + No features yet. |
| 627 | + <br /> |
| 628 | + Use the command bar to create a part. |
| 629 | + </div> |
| 630 | + ) : ( |
| 631 | + <ContextMenu> |
| 632 | + <div> |
| 633 | + {/* Assembly mode: show instances and joints */} |
| 634 | + {hasInstances ? ( |
| 635 | + <AssemblyTree |
| 636 | + instances={document.instances!} |
| 637 | + joints={document.joints ?? []} |
| 638 | + groundInstanceId={document.groundInstanceId} |
| 639 | + /> |
| 640 | + ) : ( |
| 641 | + <> |
| 642 | + {/* Legacy mode: show parts */} |
| 643 | + {parts.map((part) => ( |
| 644 | + <TreeNode |
| 645 | + key={part.id} |
| 646 | + part={part} |
| 647 | + depth={0} |
| 648 | + expandedIds={expandedIds} |
| 649 | + toggleExpanded={toggleExpanded} |
| 650 | + consumedParts={consumedParts} |
| 651 | + renamingId={renamingId} |
| 652 | + setRenamingId={setRenamingId} |
| 653 | + /> |
| 654 | + ))} |
| 655 | + </> |
| 656 | + )} |
| 657 | + </div> |
| 658 | + </ContextMenu> |
| 659 | + )} |
| 660 | + </div> |
639 | 661 | </div> |
640 | | - </div> |
| 662 | + </> |
641 | 663 | ); |
642 | 664 | } |
0 commit comments