Skip to content
File

Blob: src/client/components/workspace/share-dialog/shell.tsx

typescript143 lines
1import { useRef, type ReactNode } from "react";
2import { Globe2, Share2, Users } from "lucide-react";
3 
4import { DropdownPortal } from "@/client/components/ui/dropdown-portal";
5import { Skeleton } from "@/client/components/ui/skeleton";
6import type { ShareDialogAffordance } from "@/client/lib/affordance/share-dialog";
7 
8import { useShareDialogShell } from "./context";
9import { PublishTabContent } from "./publish-tab";
10import { ShareLinkSection, SharePeopleSection } from "./share-tab";
11import type { DialogTab } from "./types";
12 
13export 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 
70function 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 
97function 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}