react-sharesheet 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +581 -0
- package/dist/content.d.mts +10 -0
- package/dist/content.d.ts +10 -0
- package/dist/content.js +833 -0
- package/dist/content.js.map +1 -0
- package/dist/content.mjs +813 -0
- package/dist/content.mjs.map +1 -0
- package/dist/drawer.d.mts +10 -0
- package/dist/drawer.d.ts +10 -0
- package/dist/drawer.js +960 -0
- package/dist/drawer.js.map +1 -0
- package/dist/drawer.mjs +940 -0
- package/dist/drawer.mjs.map +1 -0
- package/dist/headless.d.mts +51 -0
- package/dist/headless.d.ts +51 -0
- package/dist/headless.js +405 -0
- package/dist/headless.js.map +1 -0
- package/dist/headless.mjs +364 -0
- package/dist/headless.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1040 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +991 -0
- package/dist/index.mjs.map +1 -0
- package/dist/platforms-DU1DVDFq.d.mts +280 -0
- package/dist/platforms-DU1DVDFq.d.ts +280 -0
- package/package.json +88 -0
- package/src/ShareSheetContent.tsx +538 -0
- package/src/ShareSheetDrawer.tsx +128 -0
- package/src/content.ts +4 -0
- package/src/drawer.ts +4 -0
- package/src/headless.ts +45 -0
- package/src/hooks.ts +192 -0
- package/src/index.ts +68 -0
- package/src/platforms.tsx +186 -0
- package/src/share-functions.ts +63 -0
- package/src/types.ts +236 -0
- package/src/utils.ts +15 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState, useCallback } from "react";
|
|
4
|
+
import { Image, FileText, Music, Film, Link2, Play } from "lucide-react";
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
import { useShareSheet } from "./hooks";
|
|
7
|
+
import {
|
|
8
|
+
PLATFORM_IDS,
|
|
9
|
+
PLATFORM_COLORS,
|
|
10
|
+
PLATFORM_LABELS,
|
|
11
|
+
PLATFORM_ICONS,
|
|
12
|
+
PLATFORM_CSS_VARS,
|
|
13
|
+
} from "./platforms";
|
|
14
|
+
import {
|
|
15
|
+
CSS_VARS_UI,
|
|
16
|
+
CSS_VAR_UI_DEFAULTS,
|
|
17
|
+
type ShareSheetContentProps,
|
|
18
|
+
type ShareOption,
|
|
19
|
+
type ShareButtonConfig,
|
|
20
|
+
type PreviewConfig,
|
|
21
|
+
type PreviewType,
|
|
22
|
+
} from "./types";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_BUTTON_SIZE = 45;
|
|
25
|
+
const DEFAULT_ICON_SIZE = 22;
|
|
26
|
+
|
|
27
|
+
// File extension mappings
|
|
28
|
+
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "avif"];
|
|
29
|
+
const VIDEO_EXTENSIONS = ["mp4", "webm", "mov", "avi", "mkv", "m4v", "ogv"];
|
|
30
|
+
const AUDIO_EXTENSIONS = ["mp3", "wav", "ogg", "m4a", "aac", "flac", "wma"];
|
|
31
|
+
|
|
32
|
+
// Detect content type from URL
|
|
33
|
+
function detectPreviewType(url: string): PreviewType {
|
|
34
|
+
try {
|
|
35
|
+
const pathname = new URL(url, "http://localhost").pathname;
|
|
36
|
+
const ext = pathname.split(".").pop()?.toLowerCase() || "";
|
|
37
|
+
|
|
38
|
+
if (IMAGE_EXTENSIONS.includes(ext)) return "image";
|
|
39
|
+
if (VIDEO_EXTENSIONS.includes(ext)) return "video";
|
|
40
|
+
if (AUDIO_EXTENSIONS.includes(ext)) return "audio";
|
|
41
|
+
|
|
42
|
+
// Check for common patterns
|
|
43
|
+
if (url.includes("/api/og") || url.includes("og-image")) return "image";
|
|
44
|
+
if (url.includes("youtube.com") || url.includes("vimeo.com")) return "video";
|
|
45
|
+
|
|
46
|
+
return "link";
|
|
47
|
+
} catch {
|
|
48
|
+
return "link";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get filename from URL
|
|
53
|
+
function getFilenameFromUrl(url: string): string {
|
|
54
|
+
try {
|
|
55
|
+
const pathname = new URL(url, "http://localhost").pathname;
|
|
56
|
+
const filename = pathname.split("/").pop() || "";
|
|
57
|
+
return decodeURIComponent(filename);
|
|
58
|
+
} catch {
|
|
59
|
+
return url;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Default class names
|
|
64
|
+
const defaultClasses = {
|
|
65
|
+
root: "max-w-md mx-auto",
|
|
66
|
+
header: "text-center mb-2",
|
|
67
|
+
title: "text-2xl font-black",
|
|
68
|
+
subtitle: "mt-1 text-sm",
|
|
69
|
+
preview: "flex justify-center mb-4 px-4",
|
|
70
|
+
previewSkeleton: "rounded-xl overflow-hidden",
|
|
71
|
+
previewImage: "",
|
|
72
|
+
previewVideo: "",
|
|
73
|
+
previewFile: "",
|
|
74
|
+
previewFileIcon: "",
|
|
75
|
+
previewFilename: "truncate",
|
|
76
|
+
previewLink: "",
|
|
77
|
+
grid: "px-2 py-6 flex flex-row items-center gap-4 gap-y-6 flex-wrap justify-center",
|
|
78
|
+
button: "flex flex-col items-center gap-0 text-xs w-[60px] outline-none cursor-pointer group",
|
|
79
|
+
buttonIcon: "p-2 rounded-full transition-all flex items-center justify-center group-hover:scale-110 group-active:scale-95 mb-2",
|
|
80
|
+
buttonLabel: "",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Shimmer keyframes as inline style
|
|
84
|
+
const shimmerKeyframes = `
|
|
85
|
+
@keyframes sharesheet-shimmer {
|
|
86
|
+
0% { transform: translateX(-100%); }
|
|
87
|
+
100% { transform: translateX(100%); }
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
// Helper to create var() with fallback
|
|
92
|
+
function cssVar(name: string, fallback: string): string {
|
|
93
|
+
return `var(${name}, ${fallback})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Normalize preview prop to PreviewConfig
|
|
97
|
+
function normalizePreview(preview: string | PreviewConfig | null | undefined): PreviewConfig | null {
|
|
98
|
+
if (!preview) return null;
|
|
99
|
+
|
|
100
|
+
if (typeof preview === "string") {
|
|
101
|
+
const type = detectPreviewType(preview);
|
|
102
|
+
return {
|
|
103
|
+
url: preview,
|
|
104
|
+
type,
|
|
105
|
+
filename: getFilenameFromUrl(preview),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// It's already a config object
|
|
110
|
+
const type = preview.type === "auto" || !preview.type ? detectPreviewType(preview.url) : preview.type;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...preview,
|
|
114
|
+
type,
|
|
115
|
+
filename: preview.filename || getFilenameFromUrl(preview.url),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function ShareSheetContent({
|
|
120
|
+
title = "Share",
|
|
121
|
+
shareUrl,
|
|
122
|
+
shareText,
|
|
123
|
+
preview,
|
|
124
|
+
downloadUrl,
|
|
125
|
+
downloadFilename,
|
|
126
|
+
className,
|
|
127
|
+
classNames = {},
|
|
128
|
+
buttonSize = DEFAULT_BUTTON_SIZE,
|
|
129
|
+
iconSize = DEFAULT_ICON_SIZE,
|
|
130
|
+
onNativeShare,
|
|
131
|
+
onCopy,
|
|
132
|
+
onDownload,
|
|
133
|
+
hide = [],
|
|
134
|
+
show,
|
|
135
|
+
labels = {},
|
|
136
|
+
icons = {},
|
|
137
|
+
}: ShareSheetContentProps) {
|
|
138
|
+
const [mediaLoaded, setMediaLoaded] = useState(false);
|
|
139
|
+
const [mediaError, setMediaError] = useState(false);
|
|
140
|
+
|
|
141
|
+
const handleMediaLoad = useCallback(() => {
|
|
142
|
+
setMediaLoaded(true);
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
const handleMediaError = useCallback(() => {
|
|
146
|
+
setMediaError(true);
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
// Normalize preview config
|
|
150
|
+
const previewConfig = useMemo(() => normalizePreview(preview), [preview]);
|
|
151
|
+
|
|
152
|
+
const shareSheet = useShareSheet({
|
|
153
|
+
shareUrl,
|
|
154
|
+
shareText,
|
|
155
|
+
downloadUrl,
|
|
156
|
+
downloadFilename,
|
|
157
|
+
emailSubject: title,
|
|
158
|
+
onNativeShare,
|
|
159
|
+
onCopy,
|
|
160
|
+
onDownload,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Map platform IDs to their share actions
|
|
164
|
+
const shareActions: Record<ShareOption, () => void> = useMemo(() => ({
|
|
165
|
+
native: () => void shareSheet.nativeShare(),
|
|
166
|
+
copy: () => void shareSheet.copyLink(),
|
|
167
|
+
download: () => void shareSheet.downloadFile(),
|
|
168
|
+
whatsapp: shareSheet.shareWhatsApp,
|
|
169
|
+
telegram: shareSheet.shareTelegram,
|
|
170
|
+
instagram: shareSheet.shareInstagram,
|
|
171
|
+
facebook: shareSheet.shareFacebook,
|
|
172
|
+
snapchat: shareSheet.shareSnapchat,
|
|
173
|
+
sms: shareSheet.shareSMS,
|
|
174
|
+
email: shareSheet.shareEmail,
|
|
175
|
+
linkedin: shareSheet.shareLinkedIn,
|
|
176
|
+
reddit: shareSheet.shareReddit,
|
|
177
|
+
x: shareSheet.shareX,
|
|
178
|
+
tiktok: shareSheet.shareTikTok,
|
|
179
|
+
threads: shareSheet.shareThreads,
|
|
180
|
+
}), [shareSheet]);
|
|
181
|
+
|
|
182
|
+
// Dynamic labels that depend on state
|
|
183
|
+
const dynamicLabels: Partial<Record<ShareOption, string>> = useMemo(() => ({
|
|
184
|
+
copy: shareSheet.copied ? "Copied!" : PLATFORM_LABELS.copy,
|
|
185
|
+
download: shareSheet.downloading ? "..." : PLATFORM_LABELS.download,
|
|
186
|
+
}), [shareSheet.copied, shareSheet.downloading]);
|
|
187
|
+
|
|
188
|
+
// Build button configs from platform data
|
|
189
|
+
const buttons: ShareButtonConfig[] = useMemo(() => {
|
|
190
|
+
return PLATFORM_IDS.map((id) => {
|
|
191
|
+
const Icon = PLATFORM_ICONS[id];
|
|
192
|
+
const defaultLabel = dynamicLabels[id] ?? PLATFORM_LABELS[id];
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
id,
|
|
196
|
+
label: labels[id] ?? defaultLabel,
|
|
197
|
+
icon: icons[id] ?? <Icon size={iconSize} />,
|
|
198
|
+
// Use CSS var with fallback to platform color
|
|
199
|
+
bgColor: cssVar(PLATFORM_CSS_VARS[id], PLATFORM_COLORS[id].bg),
|
|
200
|
+
textColor: PLATFORM_COLORS[id].text,
|
|
201
|
+
onClick: shareActions[id],
|
|
202
|
+
// Conditions for showing certain buttons
|
|
203
|
+
condition: id === "native" ? shareSheet.canNativeShare
|
|
204
|
+
: id === "download" ? !!downloadUrl
|
|
205
|
+
: true,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}, [iconSize, labels, icons, dynamicLabels, shareActions, shareSheet.canNativeShare, downloadUrl]);
|
|
209
|
+
|
|
210
|
+
const visibleButtons = useMemo(() => {
|
|
211
|
+
return buttons.filter((btn) => {
|
|
212
|
+
// Check condition (e.g., canNativeShare, downloadUrl exists)
|
|
213
|
+
if (btn.condition === false) return false;
|
|
214
|
+
// Filter by show list if provided
|
|
215
|
+
if (show && show.length > 0) return show.includes(btn.id);
|
|
216
|
+
// Filter by hide list
|
|
217
|
+
if (hide.includes(btn.id)) return false;
|
|
218
|
+
return true;
|
|
219
|
+
});
|
|
220
|
+
}, [buttons, show, hide]);
|
|
221
|
+
|
|
222
|
+
const showPreview = !!previewConfig;
|
|
223
|
+
|
|
224
|
+
// Render preview based on type
|
|
225
|
+
const renderPreview = () => {
|
|
226
|
+
if (!previewConfig) return null;
|
|
227
|
+
|
|
228
|
+
const { type, url, filename, alt, poster } = previewConfig;
|
|
229
|
+
const bgColor = cssVar(CSS_VARS_UI.previewBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.previewBg]);
|
|
230
|
+
const shimmerColor = cssVar(CSS_VARS_UI.previewShimmer, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.previewShimmer]);
|
|
231
|
+
const textColor = cssVar(CSS_VARS_UI.subtitleColor, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.subtitleColor]);
|
|
232
|
+
|
|
233
|
+
// Floating URL label (centered below)
|
|
234
|
+
const UrlLabel = ({ displayUrl = url }: { displayUrl?: string }) => (
|
|
235
|
+
<div
|
|
236
|
+
className={cn(defaultClasses.previewFilename, classNames.previewFilename)}
|
|
237
|
+
style={{
|
|
238
|
+
color: textColor,
|
|
239
|
+
fontSize: "10px",
|
|
240
|
+
opacity: 0.5,
|
|
241
|
+
textAlign: "center",
|
|
242
|
+
marginTop: "6px",
|
|
243
|
+
}}
|
|
244
|
+
>
|
|
245
|
+
{displayUrl}
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Placeholder card for non-media types or loading/error states
|
|
250
|
+
const PlaceholderCard = ({
|
|
251
|
+
icon: IconComponent,
|
|
252
|
+
isLoading = false,
|
|
253
|
+
label,
|
|
254
|
+
displayUrl,
|
|
255
|
+
}: {
|
|
256
|
+
icon: typeof Link2;
|
|
257
|
+
isLoading?: boolean;
|
|
258
|
+
label?: string;
|
|
259
|
+
displayUrl?: string;
|
|
260
|
+
}) => (
|
|
261
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
262
|
+
<div
|
|
263
|
+
className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
|
|
264
|
+
style={{
|
|
265
|
+
position: "relative",
|
|
266
|
+
backgroundColor: bgColor,
|
|
267
|
+
width: "200px",
|
|
268
|
+
height: "120px",
|
|
269
|
+
overflow: "hidden",
|
|
270
|
+
display: "flex",
|
|
271
|
+
alignItems: "center",
|
|
272
|
+
justifyContent: "center",
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
{isLoading && (
|
|
276
|
+
<div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
|
|
277
|
+
<div
|
|
278
|
+
style={{
|
|
279
|
+
position: "absolute",
|
|
280
|
+
inset: 0,
|
|
281
|
+
background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
|
|
282
|
+
animation: "sharesheet-shimmer 1.5s infinite",
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
<div
|
|
288
|
+
style={{
|
|
289
|
+
display: "flex",
|
|
290
|
+
flexDirection: "column",
|
|
291
|
+
alignItems: "center",
|
|
292
|
+
justifyContent: "center",
|
|
293
|
+
gap: "8px",
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
<IconComponent size={32} style={{ color: textColor, opacity: 0.4 }} />
|
|
297
|
+
{label && (
|
|
298
|
+
<span style={{ color: textColor, fontSize: "11px", opacity: 0.4 }}>
|
|
299
|
+
{label}
|
|
300
|
+
</span>
|
|
301
|
+
)}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
<UrlLabel displayUrl={displayUrl} />
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// If there was an error loading media, show fallback
|
|
309
|
+
if (mediaError && (type === "image" || type === "video")) {
|
|
310
|
+
return <PlaceholderCard icon={Link2} displayUrl={url} />;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
switch (type) {
|
|
314
|
+
case "image":
|
|
315
|
+
// Show placeholder while loading, then show image with correct aspect ratio
|
|
316
|
+
if (!mediaLoaded) {
|
|
317
|
+
return (
|
|
318
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
319
|
+
<div
|
|
320
|
+
className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
|
|
321
|
+
style={{
|
|
322
|
+
position: "relative",
|
|
323
|
+
backgroundColor: bgColor,
|
|
324
|
+
width: "200px",
|
|
325
|
+
height: "120px",
|
|
326
|
+
overflow: "hidden",
|
|
327
|
+
display: "flex",
|
|
328
|
+
alignItems: "center",
|
|
329
|
+
justifyContent: "center",
|
|
330
|
+
}}
|
|
331
|
+
>
|
|
332
|
+
<div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
|
|
333
|
+
<div
|
|
334
|
+
style={{
|
|
335
|
+
position: "absolute",
|
|
336
|
+
inset: 0,
|
|
337
|
+
background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
|
|
338
|
+
animation: "sharesheet-shimmer 1.5s infinite",
|
|
339
|
+
}}
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
342
|
+
<Image size={32} style={{ color: textColor, opacity: 0.4 }} />
|
|
343
|
+
</div>
|
|
344
|
+
<UrlLabel />
|
|
345
|
+
{/* Hidden image for preloading */}
|
|
346
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
347
|
+
<img
|
|
348
|
+
src={url}
|
|
349
|
+
alt={alt || "Preview"}
|
|
350
|
+
onLoad={handleMediaLoad}
|
|
351
|
+
onError={handleMediaError}
|
|
352
|
+
style={{ display: "none" }}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
// Image loaded - show with correct aspect ratio
|
|
358
|
+
return (
|
|
359
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
360
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
361
|
+
<img
|
|
362
|
+
src={url}
|
|
363
|
+
alt={alt || "Preview"}
|
|
364
|
+
className={cn(defaultClasses.previewImage, classNames.previewImage)}
|
|
365
|
+
style={{
|
|
366
|
+
maxWidth: "100%",
|
|
367
|
+
maxHeight: "180px",
|
|
368
|
+
borderRadius: "12px",
|
|
369
|
+
opacity: 1,
|
|
370
|
+
transition: "opacity 0.3s ease-in-out",
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
<UrlLabel />
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
case "video":
|
|
378
|
+
if (!mediaLoaded) {
|
|
379
|
+
return (
|
|
380
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
381
|
+
<div
|
|
382
|
+
className={cn(defaultClasses.previewSkeleton, classNames.previewSkeleton)}
|
|
383
|
+
style={{
|
|
384
|
+
position: "relative",
|
|
385
|
+
backgroundColor: bgColor,
|
|
386
|
+
width: "200px",
|
|
387
|
+
height: "120px",
|
|
388
|
+
overflow: "hidden",
|
|
389
|
+
display: "flex",
|
|
390
|
+
alignItems: "center",
|
|
391
|
+
justifyContent: "center",
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
<div style={{ position: "absolute", inset: 0, overflow: "hidden" }}>
|
|
395
|
+
<div
|
|
396
|
+
style={{
|
|
397
|
+
position: "absolute",
|
|
398
|
+
inset: 0,
|
|
399
|
+
background: `linear-gradient(90deg, transparent, ${shimmerColor}, transparent)`,
|
|
400
|
+
animation: "sharesheet-shimmer 1.5s infinite",
|
|
401
|
+
}}
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
<Film size={32} style={{ color: textColor, opacity: 0.4 }} />
|
|
405
|
+
</div>
|
|
406
|
+
<UrlLabel />
|
|
407
|
+
{/* Hidden video for preloading */}
|
|
408
|
+
<video
|
|
409
|
+
src={url}
|
|
410
|
+
poster={poster}
|
|
411
|
+
onLoadedData={handleMediaLoad}
|
|
412
|
+
onError={handleMediaError}
|
|
413
|
+
style={{ display: "none" }}
|
|
414
|
+
muted
|
|
415
|
+
playsInline
|
|
416
|
+
preload="metadata"
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
// Video loaded
|
|
422
|
+
return (
|
|
423
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
|
|
424
|
+
<div style={{ position: "relative", borderRadius: "12px", overflow: "hidden" }}>
|
|
425
|
+
<video
|
|
426
|
+
src={url}
|
|
427
|
+
poster={poster}
|
|
428
|
+
className={cn(defaultClasses.previewVideo, classNames.previewVideo)}
|
|
429
|
+
style={{
|
|
430
|
+
maxWidth: "100%",
|
|
431
|
+
maxHeight: "180px",
|
|
432
|
+
display: "block",
|
|
433
|
+
}}
|
|
434
|
+
muted
|
|
435
|
+
playsInline
|
|
436
|
+
preload="metadata"
|
|
437
|
+
/>
|
|
438
|
+
{/* Play icon overlay */}
|
|
439
|
+
<div
|
|
440
|
+
style={{
|
|
441
|
+
position: "absolute",
|
|
442
|
+
inset: 0,
|
|
443
|
+
display: "flex",
|
|
444
|
+
alignItems: "center",
|
|
445
|
+
justifyContent: "center",
|
|
446
|
+
pointerEvents: "none",
|
|
447
|
+
}}
|
|
448
|
+
>
|
|
449
|
+
<div
|
|
450
|
+
style={{
|
|
451
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
452
|
+
borderRadius: "50%",
|
|
453
|
+
padding: "10px",
|
|
454
|
+
}}
|
|
455
|
+
>
|
|
456
|
+
<Play size={20} fill="white" color="white" />
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
<UrlLabel />
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
case "audio":
|
|
465
|
+
return <PlaceholderCard icon={Music} label={filename || "Audio"} displayUrl={url} />;
|
|
466
|
+
|
|
467
|
+
case "file":
|
|
468
|
+
return <PlaceholderCard icon={FileText} label={filename || "File"} displayUrl={url} />;
|
|
469
|
+
|
|
470
|
+
case "link":
|
|
471
|
+
default:
|
|
472
|
+
return <PlaceholderCard icon={Link2} displayUrl={url} />;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
return (
|
|
477
|
+
<div className={cn(defaultClasses.root, classNames.root, className)}>
|
|
478
|
+
{/* Inject shimmer keyframes */}
|
|
479
|
+
<style dangerouslySetInnerHTML={{ __html: shimmerKeyframes }} />
|
|
480
|
+
|
|
481
|
+
<div className={cn(defaultClasses.header, classNames.header)}>
|
|
482
|
+
<div
|
|
483
|
+
className={cn(defaultClasses.title, classNames.title)}
|
|
484
|
+
style={{ color: cssVar(CSS_VARS_UI.titleColor, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.titleColor]) }}
|
|
485
|
+
>
|
|
486
|
+
{title}
|
|
487
|
+
</div>
|
|
488
|
+
<div
|
|
489
|
+
className={cn(defaultClasses.subtitle, classNames.subtitle)}
|
|
490
|
+
style={{ color: cssVar(CSS_VARS_UI.subtitleColor, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.subtitleColor]) }}
|
|
491
|
+
>
|
|
492
|
+
{shareText}
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
{/* Content Preview */}
|
|
497
|
+
{showPreview && (
|
|
498
|
+
<div className={cn(defaultClasses.preview, classNames.preview)}>
|
|
499
|
+
{renderPreview()}
|
|
500
|
+
</div>
|
|
501
|
+
)}
|
|
502
|
+
|
|
503
|
+
<div className={cn(defaultClasses.grid, classNames.grid)}>
|
|
504
|
+
{visibleButtons.map((btn) => (
|
|
505
|
+
<button
|
|
506
|
+
key={btn.id}
|
|
507
|
+
type="button"
|
|
508
|
+
className={cn(defaultClasses.button, classNames.button)}
|
|
509
|
+
onClick={btn.onClick}
|
|
510
|
+
>
|
|
511
|
+
<div
|
|
512
|
+
className={cn(defaultClasses.buttonIcon, classNames.buttonIcon)}
|
|
513
|
+
style={{
|
|
514
|
+
width: buttonSize,
|
|
515
|
+
height: buttonSize,
|
|
516
|
+
backgroundColor: btn.bgColor,
|
|
517
|
+
color: btn.textColor,
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
{btn.icon}
|
|
521
|
+
</div>
|
|
522
|
+
<div
|
|
523
|
+
className={cn(defaultClasses.buttonLabel, classNames.buttonLabel)}
|
|
524
|
+
style={{ color: cssVar(CSS_VARS_UI.buttonLabelColor, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.buttonLabelColor]) }}
|
|
525
|
+
>
|
|
526
|
+
{btn.label}
|
|
527
|
+
</div>
|
|
528
|
+
</button>
|
|
529
|
+
))}
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Legacy export for backwards compatibility
|
|
536
|
+
/** @deprecated Use ShareSheetContent instead */
|
|
537
|
+
export const ShareMenuContent = ShareSheetContent;
|
|
538
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Drawer } from "vaul";
|
|
5
|
+
|
|
6
|
+
import { cn } from "./utils";
|
|
7
|
+
import { ShareSheetContent } from "./ShareSheetContent";
|
|
8
|
+
import { CSS_VARS_UI, CSS_VAR_UI_DEFAULTS, type ShareSheetDrawerProps } from "./types";
|
|
9
|
+
|
|
10
|
+
// Default class names for drawer
|
|
11
|
+
const defaultDrawerClasses = {
|
|
12
|
+
overlay: "fixed inset-0 z-[70]",
|
|
13
|
+
drawer: "flex flex-col rounded-t-[14px] h-[70%] mt-24 fixed bottom-0 left-0 right-0 z-[80] border-t outline-none",
|
|
14
|
+
drawerInner: "p-4 rounded-t-[14px] flex-1 overflow-auto",
|
|
15
|
+
handle: "mx-auto w-12 h-1.5 shrink-0 rounded-full mb-6",
|
|
16
|
+
trigger: "",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Helper to create var() with fallback
|
|
20
|
+
function cssVar(name: string, fallback: string): string {
|
|
21
|
+
return `var(${name}, ${fallback})`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ShareSheetDrawer({
|
|
25
|
+
title = "Share",
|
|
26
|
+
shareUrl,
|
|
27
|
+
shareText,
|
|
28
|
+
preview,
|
|
29
|
+
downloadUrl,
|
|
30
|
+
downloadFilename,
|
|
31
|
+
disabled,
|
|
32
|
+
children,
|
|
33
|
+
open: controlledOpen,
|
|
34
|
+
onOpenChange: controlledOnOpenChange,
|
|
35
|
+
className,
|
|
36
|
+
classNames = {},
|
|
37
|
+
buttonSize,
|
|
38
|
+
iconSize,
|
|
39
|
+
onNativeShare,
|
|
40
|
+
onCopy,
|
|
41
|
+
onDownload,
|
|
42
|
+
hide,
|
|
43
|
+
show,
|
|
44
|
+
labels,
|
|
45
|
+
icons,
|
|
46
|
+
}: ShareSheetDrawerProps) {
|
|
47
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
48
|
+
|
|
49
|
+
const isControlled = controlledOpen !== undefined;
|
|
50
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
51
|
+
const setOpen = isControlled
|
|
52
|
+
? (value: boolean) => controlledOnOpenChange?.(value)
|
|
53
|
+
: setInternalOpen;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Drawer.Root open={open} onOpenChange={setOpen} shouldScaleBackground>
|
|
57
|
+
<Drawer.Trigger asChild>
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
defaultDrawerClasses.trigger,
|
|
61
|
+
classNames.trigger,
|
|
62
|
+
disabled ? "pointer-events-none opacity-50" : ""
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
</Drawer.Trigger>
|
|
68
|
+
<Drawer.Portal>
|
|
69
|
+
<Drawer.Overlay
|
|
70
|
+
className={cn(defaultDrawerClasses.overlay, classNames.overlay)}
|
|
71
|
+
style={{
|
|
72
|
+
backgroundColor: cssVar(CSS_VARS_UI.overlayBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.overlayBg]),
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
<Drawer.Content
|
|
76
|
+
className={cn(defaultDrawerClasses.drawer, classNames.drawer)}
|
|
77
|
+
style={{
|
|
78
|
+
backgroundColor: cssVar(CSS_VARS_UI.drawerBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.drawerBg]),
|
|
79
|
+
borderColor: cssVar(CSS_VARS_UI.drawerBorder, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.drawerBorder]),
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Drawer.Title className="sr-only">{title}</Drawer.Title>
|
|
83
|
+
<div
|
|
84
|
+
className={cn(defaultDrawerClasses.drawerInner, classNames.drawerInner)}
|
|
85
|
+
style={{
|
|
86
|
+
backgroundColor: cssVar(CSS_VARS_UI.drawerBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.drawerBg]),
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
<div
|
|
90
|
+
className={cn(defaultDrawerClasses.handle, classNames.handle)}
|
|
91
|
+
style={{
|
|
92
|
+
backgroundColor: cssVar(CSS_VARS_UI.handleBg, CSS_VAR_UI_DEFAULTS[CSS_VARS_UI.handleBg]),
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<ShareSheetContent
|
|
97
|
+
title={title}
|
|
98
|
+
shareUrl={shareUrl}
|
|
99
|
+
shareText={shareText}
|
|
100
|
+
preview={preview}
|
|
101
|
+
downloadUrl={downloadUrl}
|
|
102
|
+
downloadFilename={downloadFilename}
|
|
103
|
+
className={className}
|
|
104
|
+
classNames={classNames}
|
|
105
|
+
buttonSize={buttonSize}
|
|
106
|
+
iconSize={iconSize}
|
|
107
|
+
onNativeShare={() => {
|
|
108
|
+
onNativeShare?.();
|
|
109
|
+
setOpen(false);
|
|
110
|
+
}}
|
|
111
|
+
onCopy={onCopy}
|
|
112
|
+
onDownload={onDownload}
|
|
113
|
+
hide={hide}
|
|
114
|
+
show={show}
|
|
115
|
+
labels={labels}
|
|
116
|
+
icons={icons}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</Drawer.Content>
|
|
120
|
+
</Drawer.Portal>
|
|
121
|
+
</Drawer.Root>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Legacy export for backwards compatibility
|
|
126
|
+
/** @deprecated Use ShareSheetDrawer instead */
|
|
127
|
+
export const ShareMenuDrawer = ShareSheetDrawer;
|
|
128
|
+
|
package/src/content.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ShareSheetContent, ShareMenuContent } from "./ShareSheetContent";
|
|
2
|
+
export type { ShareSheetContentProps, ShareSheetContentClassNames, ShareMenuContentProps, ShareMenuContentClassNames, ShareOption } from "./types";
|
|
3
|
+
export { CSS_VARS_UI, CSS_VAR_UI_DEFAULTS, CSS_VARS, CSS_VAR_DEFAULTS } from "./types";
|
|
4
|
+
export { PLATFORM_COLORS, PLATFORM_CSS_VARS } from "./platforms";
|
package/src/drawer.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ShareSheetDrawer, ShareMenuDrawer } from "./ShareSheetDrawer";
|
|
2
|
+
export type { ShareSheetDrawerProps, ShareSheetDrawerClassNames, ShareMenuDrawerProps, ShareMenuDrawerClassNames, ShareOption } from "./types";
|
|
3
|
+
export { CSS_VARS_UI, CSS_VAR_UI_DEFAULTS, CSS_VARS, CSS_VAR_DEFAULTS } from "./types";
|
|
4
|
+
export { PLATFORM_COLORS, PLATFORM_CSS_VARS } from "./platforms";
|