fix(files): an upload is said once in any order, copy buttons speak, a link list that failed says so

- The queue holds an upload it sees land done while this tab's upload still waits for its answer, so
  the dialog's notice is the only one whichever comes first; another device's upload follows after.
- One `useCopyFeedback` for every Copy beside a value (token, share link, set-password link): Copied
  for a moment, a notice a screen reader hears, a warning when the clipboard refuses; the token
  dialog's two Copy buttons are named by what they copy.
- The share dialog's link list shows loading, and a load error with Retry, never "no links" — and no
  New link beside a list it has not seen.
- The settings texts say that signing out elsewhere or a password change revokes the API tokens.
- DESIGN.md logs the folder-link decisions; the design lab keeps only the chosen renderings of the
  Links, Tokens and Files rounds.
This commit is contained in:
2026-10-15 04:58:17 +02:00
parent 827df7494c
commit 2efa7d5a75
23 changed files with 505 additions and 773 deletions
@@ -0,0 +1,30 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { copyText } from "../lib/clipboard";
import { notify } from "../lib/notifications";
// How long a Copy button reads "Copied" after it worked.
const COPIED_MS = 2000;
/** The feedback of a Copy button that sits beside the value it copies (a token, a share link, a
* set-password link), the same at every such button: it reads "Copied" for a moment, and a notice
* says it too — the notices are a live region, so a screen reader hears what a swapped label would
* not tell it. A copy that failed is a warning, not an error: the value is on screen and
* selectable, so it is a convenience that failed, not the action.
*
* A feature file, not `ui/`: it speaks the app's notifications (as `copyItems` does). */
export function useCopyFeedback(): { copied: boolean; copy: (value: string) => Promise<void> } {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const copy = async (value: string) => {
if (await copyText(value)) {
setCopied(true);
notify({ level: "success", message: t("common.copy.done") });
window.setTimeout(() => setCopied(false), COPIED_MS);
return;
}
notify({ level: "warning", message: t("common.copy.failed") });
};
return { copied, copy };
}