Label document share as copy link
Sharing only copies the viewer URL, so rename the action and expose the same control on each documents list row. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -18,7 +18,10 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { Toast } from "@base-ui/react/toast";
|
||||
import { LinkSimpleIcon } from "@phosphor-icons/react";
|
||||
import { Checkbox } from "@probo/ui/src/v2/Checkbox/Checkbox";
|
||||
import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton";
|
||||
import { ListItem } from "@probo/ui/src/v2/List/ListItem";
|
||||
import { ListItemContent } from "@probo/ui/src/v2/List/ListItemContent";
|
||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
@@ -27,6 +30,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
|
||||
import { DocumentAccessAction } from "./DocumentAccessAction";
|
||||
import { documentEntry } from "./variants";
|
||||
|
||||
interface DocumentEntryProps {
|
||||
// Primary line (document title, file name, or framework name).
|
||||
@@ -51,8 +55,9 @@ interface DocumentEntryProps {
|
||||
}
|
||||
|
||||
// Presentational row shared by the document / file / report list items: a title
|
||||
// with accent metadata and the trailing access action. On small screens the
|
||||
// whole row is the hit target (the trailing icon is a status affordance only).
|
||||
// with accent metadata, a copy-link control, and the trailing access action. On
|
||||
// small screens the whole row is the hit target (the trailing icon is a status
|
||||
// affordance only).
|
||||
export function DocumentEntry({
|
||||
title,
|
||||
meta,
|
||||
@@ -65,6 +70,8 @@ export function DocumentEntry({
|
||||
onSelectedChange,
|
||||
}: DocumentEntryProps) {
|
||||
const { t } = useTranslation("documents");
|
||||
const toast = Toast.useToastManager();
|
||||
const slots = documentEntry();
|
||||
|
||||
// Only locked rows (not yet authorized, no pending request) can be requested,
|
||||
// so only they are selectable — the checkbox is disabled otherwise to avoid a
|
||||
@@ -77,6 +84,14 @@ export function DocumentEntry({
|
||||
? t("actions.view")
|
||||
: t("actions.getAccess");
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const url = new URL(viewHref, window.location.origin);
|
||||
navigator.clipboard.writeText(url.href).then(
|
||||
() => toast.add({ title: t("viewer.linkCopied"), type: "success" }),
|
||||
() => {},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
className={[
|
||||
@@ -105,31 +120,42 @@ export function DocumentEntry({
|
||||
</Text>
|
||||
</ListItemContent>
|
||||
|
||||
{/* Desktop: labeled interactive control. */}
|
||||
<div className="max-sm:hidden">
|
||||
<DocumentAccessAction
|
||||
isAuthorized={isAuthorized}
|
||||
requested={requested}
|
||||
viewHref={viewHref}
|
||||
onGetAccess={onGetAccess}
|
||||
isRequesting={isRequesting}
|
||||
/>
|
||||
</div>
|
||||
<div className={slots.trailing()}>
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
aria-label={t("viewer.copyLink")}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
<LinkSimpleIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Mobile: status icon only; the row overlay handles activation.
|
||||
Pending (`requested`) rows keep the icon in the a11y tree as status. */}
|
||||
<div
|
||||
className="hidden shrink-0 max-sm:block"
|
||||
aria-hidden={mobileHitLabel != null ? true : undefined}
|
||||
>
|
||||
<DocumentAccessAction
|
||||
isAuthorized={isAuthorized}
|
||||
requested={requested}
|
||||
viewHref={viewHref}
|
||||
onGetAccess={onGetAccess}
|
||||
isRequesting={isRequesting}
|
||||
interactive={false}
|
||||
/>
|
||||
{/* Desktop: labeled interactive control. */}
|
||||
<div className={slots.accessDesktop()}>
|
||||
<DocumentAccessAction
|
||||
isAuthorized={isAuthorized}
|
||||
requested={requested}
|
||||
viewHref={viewHref}
|
||||
onGetAccess={onGetAccess}
|
||||
isRequesting={isRequesting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile: status icon only; the row overlay handles activation.
|
||||
Pending (`requested`) rows keep the icon in the a11y tree as status. */}
|
||||
<div
|
||||
className={slots.accessMobile()}
|
||||
aria-hidden={mobileHitLabel != null ? true : undefined}
|
||||
>
|
||||
<DocumentAccessAction
|
||||
isAuthorized={isAuthorized}
|
||||
requested={requested}
|
||||
viewHref={viewHref}
|
||||
onGetAccess={onGetAccess}
|
||||
isRequesting={isRequesting}
|
||||
interactive={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sit above the row content so title / icon areas are part of the hit target. */}
|
||||
|
||||
@@ -23,9 +23,9 @@ import {
|
||||
CaretLeftIcon,
|
||||
CaretRightIcon,
|
||||
DownloadSimpleIcon,
|
||||
LinkSimpleIcon,
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
ShareNetworkIcon,
|
||||
SpinnerGapIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { Button } from "@probo/ui/src/v2/Button/Button";
|
||||
@@ -64,8 +64,8 @@ interface DocumentViewerProps {
|
||||
}
|
||||
|
||||
// Full-page document viewer: a header band with the title and a toolbar
|
||||
// (page navigation + zoom for PDFs, share, download) above the scrollable body.
|
||||
// PDFs render with react-pdf, images inline, and anything else offers a
|
||||
// (page navigation + zoom for PDFs, copy link, download) above the scrollable
|
||||
// body. PDFs render with react-pdf, images inline, and anything else offers a
|
||||
// download.
|
||||
export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerProps) {
|
||||
const { t } = useTranslation("documents");
|
||||
@@ -87,7 +87,7 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP
|
||||
setCurrentPage(next);
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
const handleCopyLink = () => {
|
||||
navigator.clipboard.writeText(window.location.href).then(
|
||||
() => toast.add({ title: t("viewer.linkCopied"), type: "success" }),
|
||||
() => {},
|
||||
@@ -168,11 +168,11 @@ export function DocumentViewer({ title, dataUri, downloadName }: DocumentViewerP
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
iconStart={<ShareNetworkIcon />}
|
||||
onClick={handleShare}
|
||||
aria-label={t("viewer.share")}
|
||||
iconStart={<LinkSimpleIcon />}
|
||||
onClick={handleCopyLink}
|
||||
aria-label={t("viewer.copyLink")}
|
||||
>
|
||||
<span className={slots.actionLabel()}>{t("viewer.share")}</span>
|
||||
<span className={slots.actionLabel()}>{t("viewer.copyLink")}</span>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className={slots.separator()} />
|
||||
<Button
|
||||
|
||||
@@ -30,6 +30,16 @@ export const documentSection = tv({
|
||||
},
|
||||
});
|
||||
|
||||
// Document list row trailing chrome: copy-link + access action. z-2 sits above
|
||||
// the mobile full-row overlay so these controls stay tappable on small screens.
|
||||
export const documentEntry = tv({
|
||||
slots: {
|
||||
trailing: "relative z-2 flex shrink-0 items-center gap-1",
|
||||
accessDesktop: "max-sm:hidden",
|
||||
accessMobile: "hidden shrink-0 max-sm:block",
|
||||
},
|
||||
});
|
||||
|
||||
// PDF preview: a scrollable grey stage holding the stacked, centered pages.
|
||||
export const pdfPreview = tv({
|
||||
slots: {
|
||||
@@ -48,7 +58,7 @@ export const documentViewer = tv({
|
||||
root: "flex h-full min-h-0 flex-1 flex-col",
|
||||
header: "flex w-full flex-col gap-3",
|
||||
back: "-ml-2 self-start",
|
||||
// Stay on one row so page/zoom controls and share/download share a baseline;
|
||||
// Stay on one row so page/zoom controls and copy/download share a baseline;
|
||||
// icon-only action labels on max-sm keep this viable on phones.
|
||||
toolbar: "flex min-h-16 flex-wrap items-center justify-between gap-x-4 gap-y-2",
|
||||
toolbarStart: "flex min-w-0 flex-wrap items-center gap-2",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Dokumente",
|
||||
"share": "Dokument teilen",
|
||||
"copyLink": "Link kopieren",
|
||||
"linkCopied": "Link in die Zwischenablage kopiert",
|
||||
"download": "Herunterladen",
|
||||
"previewUnavailable": "Vorschau nicht verfügbar",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Documents",
|
||||
"share": "Share document",
|
||||
"copyLink": "Copy link",
|
||||
"linkCopied": "Link copied to clipboard",
|
||||
"download": "Download",
|
||||
"previewUnavailable": "Preview unavailable",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Documentos",
|
||||
"share": "Compartir documento",
|
||||
"copyLink": "Copiar enlace",
|
||||
"linkCopied": "Enlace copiado al portapapeles",
|
||||
"download": "Descargar",
|
||||
"previewUnavailable": "Vista previa no disponible",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Documents",
|
||||
"share": "Partager le document",
|
||||
"copyLink": "Copier le lien",
|
||||
"linkCopied": "Lien copié dans le presse-papiers",
|
||||
"download": "Télécharger",
|
||||
"previewUnavailable": "Aperçu indisponible",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Dokumen",
|
||||
"share": "Bagikan dokumen",
|
||||
"copyLink": "Salin tautan",
|
||||
"linkCopied": "Tautan disalin ke clipboard",
|
||||
"download": "Unduh",
|
||||
"previewUnavailable": "Pratinjau tidak tersedia",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Documenti",
|
||||
"share": "Condividi documento",
|
||||
"copyLink": "Copia collegamento",
|
||||
"linkCopied": "Link copiato negli appunti",
|
||||
"download": "Scarica",
|
||||
"previewUnavailable": "Anteprima non disponibile",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "ドキュメント",
|
||||
"share": "ドキュメントを共有",
|
||||
"copyLink": "リンクをコピー",
|
||||
"linkCopied": "リンクをクリップボードにコピーしました",
|
||||
"download": "ダウンロード",
|
||||
"previewUnavailable": "プレビューは利用できません",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "문서",
|
||||
"share": "문서 공유",
|
||||
"copyLink": "링크 복사",
|
||||
"linkCopied": "링크가 클립보드에 복사되었습니다",
|
||||
"download": "다운로드",
|
||||
"previewUnavailable": "미리보기를 사용할 수 없습니다",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Dokumenty",
|
||||
"share": "Udostępnij dokument",
|
||||
"copyLink": "Kopiuj link",
|
||||
"linkCopied": "Link skopiowany do schowka",
|
||||
"download": "Pobierz",
|
||||
"previewUnavailable": "Podgląd niedostępny",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Documentos",
|
||||
"share": "Partilhar documento",
|
||||
"copyLink": "Copiar ligação",
|
||||
"linkCopied": "Link copiado para a área de transferência",
|
||||
"download": "Transferir",
|
||||
"previewUnavailable": "Pré-visualização indisponível",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Belgeler",
|
||||
"share": "Belgeyi paylaş",
|
||||
"copyLink": "Bağlantıyı kopyala",
|
||||
"linkCopied": "Bağlantı panoya kopyalandı",
|
||||
"download": "İndir",
|
||||
"previewUnavailable": "Önizleme kullanılamıyor",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "Документи",
|
||||
"share": "Поділитися документом",
|
||||
"copyLink": "Копіювати посилання",
|
||||
"linkCopied": "Посилання скопійовано до буфера обміну",
|
||||
"download": "Завантажити",
|
||||
"previewUnavailable": "Попередній перегляд недоступний",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"viewer": {
|
||||
"back": "文档",
|
||||
"share": "分享文档",
|
||||
"copyLink": "复制链接",
|
||||
"linkCopied": "链接已复制到剪贴板",
|
||||
"download": "下载",
|
||||
"previewUnavailable": "无法预览",
|
||||
|
||||
Reference in New Issue
Block a user