feat(studio): hand an export straight to a social network

EXPORT only ever wrote a file, and posting it meant leaving the studio, finding
the file and uploading it again. The dialog now offers FACEBOOK, INSTAGRAM,
REDDIT and X beside the sizes, and the render goes to the network instead of the
download folder.

What the browser can do decides how, since a page may not touch another origin's
composer: a phone's share sheet opens the target app with the picture already
attached, a desktop gets the picture on the clipboard and the site in a new tab
for one paste, and an origin with no clipboard API downloads the file beside the
open site. The line under the row says so before the click.
This commit is contained in:
2026-10-08 14:38:22 +07:00
parent 67a6a2d688
commit bb4e574486
6 changed files with 139 additions and 2 deletions
+28
View File
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useI18n } from '../i18n/I18nProvider';
import { SHARE_SITES, type ShareSite } from '../engine/share';
// Export size. The photo on the stage has a longest edge; the three fixed
// answers name the sizes people ask for by name (2048/4096), the fourth takes a
@@ -12,6 +13,7 @@ export function ExportModal({
longest,
maxDim,
onPick,
onShare,
onClose,
}: {
/** Longest edge of the photo as it stands, crop included. */
@@ -19,6 +21,10 @@ export function ExportModal({
/** The tier's own ceiling, or null for none. */
maxDim: number | null;
onPick: (target: number | null) => void;
/** Hand the file straight to a network. Always the photo unchanged: the
sites resize to their own feed anyway, so an upscale here is pixels
bought for nothing. */
onShare: (site: ShareSite) => void;
onClose: () => void;
}) {
const { t } = useI18n();
@@ -85,6 +91,28 @@ export function ExportModal({
{t('export.cap', { px: maxDim })}
</p>
)}
{/* Straight to a network, no file on the way. What happens next is the
browser's to decide — see engine/share — so the line under the row
says which of the three it will be before the click. */}
<div className="export-share" data-key="export-share">
<p className="hint">{t('export.share')}</p>
<div className="export-share-row">
{(Object.keys(SHARE_SITES) as ShareSite[]).map((site) => (
<button
key={site}
type="button"
className="btn ghost"
data-key={`export-share-${site}`}
onClick={() => onShare(site)}
>
{site === 'x' ? 'X' : site.toUpperCase()}
</button>
))}
</div>
<p className="hint" data-key="export-share-hint">
{t('export.shareHint')}
</p>
</div>
<button type="button" className="btn ghost" data-key="export-cancel" onClick={onClose}>
{t('clear.cancel')}
</button>