feat(design): glass surfaces by role, the static Badge, LinkButton, user pickers on the Combobox (E28 S3d1)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
@@ -12,9 +12,10 @@ import { formatDate } from "../lib/format";
|
||||
import { api, type DownloadJob, type ShareLink, type ShareRecipient } from "../lib/api";
|
||||
import { Button } from "./ui/Button";
|
||||
import { IconButton } from "./ui/IconButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
import { Input, InnerInput } from "./ui/Input";
|
||||
import { CheckRow } from "./ui/NativeControls";
|
||||
import { SURFACE } from "./ui/surfaces";
|
||||
import UserPicker from "./UserPicker";
|
||||
const EXPIRY_DAYS = [null, 1, 7, 30] as const;
|
||||
|
||||
// --- share with a registered user (ACL) -----------------------------------------------------
|
||||
@@ -25,27 +26,13 @@ const NO_RECIPIENTS: ShareRecipient[] = [];
|
||||
|
||||
function UserShare({ job }: { job: DownloadJob }) {
|
||||
const { t } = useTranslation();
|
||||
const [q, setQ] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const recipients = useQuery({ queryKey: qk.shareRecipients(), queryFn: api.shareRecipients });
|
||||
const list = recipients.data ?? NO_RECIPIENTS;
|
||||
const filtered = useMemo(() => {
|
||||
const s = q.trim().toLowerCase();
|
||||
if (!s) return list.slice(0, 8);
|
||||
return list
|
||||
.filter(
|
||||
(u) =>
|
||||
u.email.toLowerCase().includes(s) || (u.display_name ?? "").toLowerCase().includes(s),
|
||||
)
|
||||
.slice(0, 8);
|
||||
}, [q, list]);
|
||||
|
||||
const share = useMutation({
|
||||
mutationFn: (email: string) => api.shareDownload(job.id, email),
|
||||
onSuccess: (r) => {
|
||||
notify({ level: "success", message: t("downloads.share.done", { email: r.shared_with }) });
|
||||
setQ("");
|
||||
setOpen(false);
|
||||
},
|
||||
onError: (e) => notifyActionError(e, t("downloads.share.failed")),
|
||||
});
|
||||
@@ -56,36 +43,15 @@ function UserShare({ job }: { job: DownloadJob }) {
|
||||
<UserPlus className="w-4 h-4 text-muted" /> {t("downloads.share.userSection")}
|
||||
</div>
|
||||
<p className="text-xs text-muted mb-2">{t("downloads.share.userHint")}</p>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="w-full"
|
||||
value={q}
|
||||
onChange={(e) => {
|
||||
setQ(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
// Close on blur, but after a tick so a click on a dropdown row still registers.
|
||||
onBlur={() => setTimeout(() => setOpen(false), 150)}
|
||||
placeholder={t("downloads.share.pickPlaceholder")}
|
||||
/>
|
||||
{open && filtered.length > 0 && (
|
||||
<div className="absolute z-base mt-1 w-full rounded-lg glass-menu overflow-hidden">
|
||||
{filtered.map((u) => (
|
||||
<Pressable
|
||||
key={u.id}
|
||||
layer
|
||||
onClick={() => share.mutate(u.email)}
|
||||
disabled={share.isPending}
|
||||
className="w-full px-3 py-2 text-sm flex flex-col"
|
||||
>
|
||||
<span className="truncate">{u.display_name || u.email}</span>
|
||||
{u.display_name && <span className="text-xs text-muted truncate">{u.email}</span>}
|
||||
</Pressable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<UserPicker
|
||||
users={list}
|
||||
loading={recipients.isLoading}
|
||||
onPick={(u) => {
|
||||
if (!share.isPending) share.mutate(u.email);
|
||||
}}
|
||||
label={t("downloads.share.userSection")}
|
||||
placeholder={t("downloads.share.pickPlaceholder")}
|
||||
/>
|
||||
{recipients.data && list.length === 0 && (
|
||||
<p className="text-xs text-muted mt-1">{t("downloads.share.noUsers")}</p>
|
||||
)}
|
||||
@@ -142,7 +108,7 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg glass-card p-2.5 space-y-2">
|
||||
<div className={`${SURFACE.card} p-2.5 space-y-2`}>
|
||||
<div className="flex items-center gap-2 rounded-md focus-within:outline-2 focus-within:outline-accent/60 focus-within:outline-offset-2">
|
||||
<Link2 className="w-4 h-4 text-muted shrink-0" />
|
||||
<InnerInput className="text-xs text-muted truncate" readOnly value={fullUrl} />
|
||||
|
||||
Reference in New Issue
Block a user