File
Blob: src/client/components/workspace/share-dialog/shell.tsx
| 1 | import { useRef, type ReactNode } from "react"; |
| 2 | import { Globe2, Share2, Users } from "lucide-react"; |
| 3 | |
| 4 | import { DropdownPortal } from "@/client/components/ui/dropdown-portal"; |
| 5 | import { Skeleton } from "@/client/components/ui/skeleton"; |
| 6 | import type { ShareDialogAffordance } from "@/client/lib/affordance/share-dialog"; |
| 7 | |
| 8 | import { useShareDialogShell } from "./context"; |
| 9 | import { PublishTabContent } from "./publish-tab"; |
| 10 | import { ShareLinkSection, SharePeopleSection } from "./share-tab"; |
| 11 | import type { DialogTab } from "./types"; |
| 12 | |
| 13 | export function ShareDialogShell() { |
| 14 | const { |
| 15 | disabled, |
| 16 | title, |
| 17 | open, |
| 18 | close, |
| 19 | toggleOpen, |
| 20 | loading, |
| 21 | error, |
| 22 | dialogAffordance, |
| 23 | publishAffordance, |
| 24 | activeTab, |
| 25 | setActiveTab, |
| 26 | } = useShareDialogShell(); |
| 27 | const triggerRef = useRef<HTMLButtonElement>(null); |
| 28 | |
| 29 | const showPublishTab = publishAffordance.showPublishTab; |
| 30 | |
| 31 | return ( |
| 32 | <> |
| 33 | <button |
| 34 | ref={triggerRef} |
| 35 | onClick={toggleOpen} |
| 36 | disabled={disabled} |
| 37 | title={title} |
| 38 | className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200 disabled:cursor-not-allowed disabled:opacity-50" |
| 39 | > |
| 40 | <Share2 className="h-3.5 w-3.5" /> |
| 41 | Share |
| 42 | </button> |
| 43 | |
| 44 | {open && !disabled && ( |
| 45 | <DropdownPortal triggerRef={triggerRef} align="right" width={320} onClose={close} className="rounded-lg p-3"> |
| 46 | {showPublishTab && ( |
| 47 | <div className="mb-3 flex items-center gap-1 border-b border-zinc-700/60"> |
| 48 | <DialogTabButton tab="share" active={activeTab === "share"} onSelect={setActiveTab}> |
| 49 | <Users className="h-3.5 w-3.5" /> |
| 50 | Share |
| 51 | </DialogTabButton> |
| 52 | <DialogTabButton tab="publish" active={activeTab === "publish"} onSelect={setActiveTab}> |
| 53 | <Globe2 className="h-3.5 w-3.5" /> |
| 54 | Publish |
| 55 | </DialogTabButton> |
| 56 | </div> |
| 57 | )} |
| 58 | |
| 59 | {activeTab === "share" || !showPublishTab ? ( |
| 60 | <ShareTabContent loading={loading} error={error} dialogAffordance={dialogAffordance} /> |
| 61 | ) : ( |
| 62 | <PublishTabContent /> |
| 63 | )} |
| 64 | </DropdownPortal> |
| 65 | )} |
| 66 | </> |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | function DialogTabButton({ |
| 71 | tab, |
| 72 | active, |
| 73 | onSelect, |
| 74 | children, |
| 75 | }: { |
| 76 | tab: DialogTab; |
| 77 | active: boolean; |
| 78 | onSelect: (tab: DialogTab) => void; |
| 79 | children: ReactNode; |
| 80 | }) { |
| 81 | return ( |
| 82 | <button |
| 83 | type="button" |
| 84 | role="tab" |
| 85 | aria-selected={active} |
| 86 | onClick={() => onSelect(tab)} |
| 87 | className={[ |
| 88 | "flex items-center gap-1.5 px-2 pb-2 pt-1 text-sm transition-colors", |
| 89 | active ? "border-b-2 border-accent-500/70 text-zinc-100" : "text-zinc-500 hover:text-zinc-300", |
| 90 | ].join(" ")} |
| 91 | > |
| 92 | {children} |
| 93 | </button> |
| 94 | ); |
| 95 | } |
| 96 | |
| 97 | function ShareTabContent({ |
| 98 | loading, |
| 99 | error, |
| 100 | dialogAffordance, |
| 101 | }: { |
| 102 | loading: boolean; |
| 103 | error: string | null; |
| 104 | dialogAffordance: ShareDialogAffordance; |
| 105 | }) { |
| 106 | if (loading) { |
| 107 | return ( |
| 108 | <div aria-busy="true"> |
| 109 | <div className="mb-3"> |
| 110 | <div className="mb-1.5 flex items-center gap-1.5"> |
| 111 | <Skeleton className="h-3 w-3 rounded-sm" /> |
| 112 | <Skeleton className="h-3.5 w-14" /> |
| 113 | </div> |
| 114 | <div className="mb-2 flex items-center gap-1.5"> |
| 115 | <Skeleton className="h-8 flex-1 rounded-md" /> |
| 116 | <Skeleton className="h-8 w-20 rounded-md" /> |
| 117 | <Skeleton className="h-8 w-12 rounded-md" /> |
| 118 | </div> |
| 119 | <div className="space-y-1"> |
| 120 | <Skeleton className="h-7 w-full rounded-md" /> |
| 121 | <Skeleton className="h-7 w-full rounded-md" /> |
| 122 | </div> |
| 123 | </div> |
| 124 | <div className="border-t border-zinc-700/50 pt-3"> |
| 125 | <div className="mb-1.5 flex items-center gap-1.5"> |
| 126 | <Skeleton className="h-3 w-3 rounded-sm" /> |
| 127 | <Skeleton className="h-3.5 w-10" /> |
| 128 | </div> |
| 129 | <Skeleton className="h-8 w-full rounded-md" /> |
| 130 | </div> |
| 131 | </div> |
| 132 | ); |
| 133 | } |
| 134 | |
| 135 | return ( |
| 136 | <> |
| 137 | {error && <p className="mb-2 text-sm text-red-400">{error}</p>} |
| 138 | {dialogAffordance.showPeopleSection && <SharePeopleSection />} |
| 139 | {dialogAffordance.showLinkSection && <ShareLinkSection />} |
| 140 | </> |
| 141 | ); |
| 142 | } |