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:
@@ -2,7 +2,6 @@ import clsx from "clsx";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
Copy,
|
||||
Folder,
|
||||
KeyRound,
|
||||
@@ -12,15 +11,16 @@ import {
|
||||
SlidersHorizontal,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Decision, Sample, Variant } from "./labKit";
|
||||
import { Reference, Sample } from "./labKit";
|
||||
import { Badge } from "../components/ui/Badge";
|
||||
import { Chip } from "../components/ui/Chip";
|
||||
import { SURFACE } from "../components/ui/surfaces";
|
||||
import { buttonClass, iconButtonClass } from "../components/ui/buttonStyles";
|
||||
import { fieldClass } from "../components/ui/fieldStyles";
|
||||
|
||||
// The API tokens round: how a machine (the hub) gets a key to the Files library. Static markup on
|
||||
// purpose (the lab's rule); the sample tokens are the owner's own kind (the hub, a phone script).
|
||||
// API tokens, DECIDED (owner A/A/A): how a machine (the hub) gets a key to the Files library. Static
|
||||
// markup on purpose (the lab's rule); the sample tokens are the owner's own kind (the hub, a phone
|
||||
// script).
|
||||
|
||||
const ICON = iconButtonClass({ variant: "ghost", size: "sm" });
|
||||
const noop = () => {};
|
||||
@@ -143,49 +143,6 @@ function ListRows() {
|
||||
);
|
||||
}
|
||||
|
||||
function ListTable() {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs">
|
||||
<thead className="text-muted">
|
||||
<tr className="border-b border-border/60">
|
||||
<th className="py-1.5 pr-3 font-medium">Név</th>
|
||||
<th className="py-1.5 pr-3 font-medium">Mappák</th>
|
||||
<th className="py-1.5 pr-3 font-medium">Használva</th>
|
||||
<th className="py-1.5 pr-3 font-medium">Lejár</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{TOKENS.map((tk) => (
|
||||
<tr key={tk.prefix} className="border-b border-border/40 align-top">
|
||||
<td className="py-2 pr-3">
|
||||
<div className="text-sm font-medium">{tk.name}</div>
|
||||
<code className="text-muted">{tk.prefix}…</code>
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-muted">{tk.grants.map((g) => g.folder).join(", ")}</td>
|
||||
<td className="py-2 pr-3 text-muted whitespace-nowrap">{tk.used}</td>
|
||||
<td
|
||||
className={clsx(
|
||||
"py-2 pr-3 whitespace-nowrap",
|
||||
tk.soon ? "text-warn" : "text-muted",
|
||||
)}
|
||||
>
|
||||
{tk.expires}
|
||||
</td>
|
||||
<td className="py-1">
|
||||
<span className={ICON} aria-label="Műveletek">
|
||||
<MoreHorizontal className="size-4" />
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── T2 · the new-token dialog ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function Dialog({
|
||||
@@ -220,22 +177,16 @@ function Field({ label, children }: { label: string; children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Expiry({ never = false }: { never?: boolean }) {
|
||||
function Expiry() {
|
||||
return (
|
||||
<Field label="Lejárat">
|
||||
<span className="flex flex-wrap gap-1">
|
||||
{["30 nap", "90 nap", "1 év", "Soha"].map((l) => (
|
||||
<Chip key={l} pressed={never ? l === "Soha" : l === "1 év"} onClick={noop}>
|
||||
<Chip key={l} pressed={l === "1 év"} onClick={noop}>
|
||||
{l}
|
||||
</Chip>
|
||||
))}
|
||||
</span>
|
||||
{never && (
|
||||
<span className="flex items-start gap-1.5 text-xs text-warn">
|
||||
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" />
|
||||
Egy soha le nem járó token addig nyitja a mappát, amíg vissza nem vonod.
|
||||
</span>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -325,30 +276,6 @@ function PresetFirst({ custom }: { custom: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
function OneForm() {
|
||||
return (
|
||||
<Dialog
|
||||
title="Új API-token"
|
||||
foot={
|
||||
<>
|
||||
<span className={buttonClass({ variant: "outline", size: "sm" })}>Mégse</span>
|
||||
<span className={buttonClass({ variant: "primary", size: "sm" })}>Létrehozás</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Field label="Név">
|
||||
<span className={clsx(fieldClass({ size: "sm" }), "flex items-center")}>Hub csere</span>
|
||||
</Field>
|
||||
<span className="flex items-center gap-2 text-xs text-muted">
|
||||
Mappák és jogok
|
||||
<span className={buttonClass({ variant: "link", size: "none" })}>Hub csere kitöltése</span>
|
||||
</span>
|
||||
<RightsMatrix />
|
||||
<Expiry never />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// ── T3 · the token, shown once ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const RAW = "sfl_7Qm2xK9aVt3pLc8RzN0eWq5uYb1HdJ6gFs4kMx2oTi9a3F7kQe";
|
||||
@@ -396,120 +323,48 @@ function ShownInDialog() {
|
||||
);
|
||||
}
|
||||
|
||||
function ShownAsBanner() {
|
||||
return (
|
||||
<SettingsFrame>
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-accent/50 bg-accent/6 p-3">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium">
|
||||
<Check className="size-4 text-success" />
|
||||
„Hub csere” elkészült — másold ki most, később nem látod.
|
||||
</span>
|
||||
<CopyRow>{RAW}</CopyRow>
|
||||
<span className={clsx(buttonClass({ variant: "link", size: "none" }), "self-start")}>
|
||||
curl példa másolása
|
||||
</span>
|
||||
</div>
|
||||
<ListRows />
|
||||
</SettingsFrame>
|
||||
);
|
||||
}
|
||||
|
||||
// ── The round ───────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function TokensRound() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<Decision
|
||||
id="open-tokens-t1"
|
||||
<div className="flex flex-col gap-4">
|
||||
<Reference
|
||||
id="tokens-t1"
|
||||
code="T1"
|
||||
title="The token list (Settings → API tokens)"
|
||||
question="How does the tab list the tokens: what each opens, when it was last used, when it runs out? Revoke is in each row's “…” menu, behind a confirm."
|
||||
now="There is no token UI; tokens exist only through the API."
|
||||
rule="A row per token in three lines: name + prefix (the “soon expires” badge beside the name), then every folder with its rights, then last use and expiry — nothing truncated at the ~36rem Settings column, and it fits a phone as is. Revoke is in the row's ⋯ menu, behind a confirm."
|
||||
>
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<Variant
|
||||
letter="A"
|
||||
name="Rows, three lines each"
|
||||
recommended
|
||||
why="The Settings column is ~36rem: a row reads name + prefix, then every folder with its rights, then last use and expiry — nothing truncated, and the “soon expires” badge sits beside the name. Fits a phone as is."
|
||||
>
|
||||
<SettingsFrame>
|
||||
<ListRows />
|
||||
</SettingsFrame>
|
||||
</Variant>
|
||||
<Variant
|
||||
letter="B"
|
||||
name="A table"
|
||||
why="Scans in columns like the Download formats table, but at this width the folders column truncates to names (rights drop out) and a phone needs the card fallback anyway."
|
||||
>
|
||||
<SettingsFrame>
|
||||
<ListTable />
|
||||
</SettingsFrame>
|
||||
</Variant>
|
||||
</div>
|
||||
</Decision>
|
||||
<SettingsFrame>
|
||||
<ListRows />
|
||||
</SettingsFrame>
|
||||
</Reference>
|
||||
|
||||
<Decision
|
||||
id="open-tokens-t2"
|
||||
<Reference
|
||||
id="tokens-t2"
|
||||
code="T2"
|
||||
title="Making a token"
|
||||
question="How do you make one: the one-click Hub csere, or a token of your own folders and rights? Expiry: 30 days, 90 days, 1 year (default) or never (with a warning)."
|
||||
now="Nothing in the app."
|
||||
rule="The dialog asks the kind first. Hub csere: one tile, the name filled in, Create — the common token never shows a matrix to get wrong. Egyedi: each folder a row, its rights as chips; a source folder has no Write (nothing can be uploaded into it, the server refuses it too). Expiry is a segmented control (DESIGN.md “A choice between a few values”): 30 days, 90 days, 1 year (the default) or never, which warns that the token opens the folder until revoked."
|
||||
>
|
||||
<div className="grid gap-4 xl:grid-cols-3">
|
||||
<Variant
|
||||
letter="A"
|
||||
name="Pick the kind first — Hub csere"
|
||||
recommended
|
||||
why="The hub is the case you have: one tile, the name filled in, Create. The rights grid only appears for “Egyedi”, so the common token never shows a matrix to get wrong."
|
||||
>
|
||||
<div className="grid items-start gap-4 xl:grid-cols-2">
|
||||
<Sample label="Hub csere">
|
||||
<PresetFirst custom={false} />
|
||||
</Variant>
|
||||
<Variant
|
||||
letter="A"
|
||||
name="…the same, Egyedi"
|
||||
recommended
|
||||
why="Each folder a row, its rights as chips. A source folder has no Write: nothing can be uploaded into it (the server refuses it too)."
|
||||
>
|
||||
</Sample>
|
||||
<Sample label="Egyedi">
|
||||
<PresetFirst custom />
|
||||
</Variant>
|
||||
<Variant
|
||||
letter="B"
|
||||
name="One form, a preset link"
|
||||
why="Everything at once, with “Hub csere kitöltése” filling the grid. Fewer clicks for a custom token, but the hub case starts from a matrix. Shown with “never” picked, and its warning."
|
||||
>
|
||||
<OneForm />
|
||||
</Variant>
|
||||
</Sample>
|
||||
</div>
|
||||
</Decision>
|
||||
</Reference>
|
||||
|
||||
<Decision
|
||||
id="open-tokens-t3"
|
||||
<Reference
|
||||
id="tokens-t3"
|
||||
code="T3"
|
||||
title="The token, shown once"
|
||||
question="Only its fingerprint is stored, so the token can be copied once. Where does it show, with a ready curl line to try it?"
|
||||
now="Nothing in the app."
|
||||
rule="Only its fingerprint is stored, so the token shows once, as the dialog's last step: the token with a Copy, a curl line to try it, and Kész. Closing the dialog is plainly the moment it is gone."
|
||||
>
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<Variant
|
||||
letter="A"
|
||||
name="The dialog's last step"
|
||||
recommended
|
||||
why="You are already looking at the dialog: the token, a Copy, a curl line to try it, and “Kész”. Closing is the moment it is gone, and it is plain that it is."
|
||||
>
|
||||
<Sample label="after Létrehozás">
|
||||
<ShownInDialog />
|
||||
</Sample>
|
||||
</Variant>
|
||||
<Variant
|
||||
letter="B"
|
||||
name="A banner above the list"
|
||||
why="The dialog closes and the token waits above the list until you leave the tab. The new row is right under it, but a banner is easy to scroll past — and leaving the tab loses it for good."
|
||||
>
|
||||
<ShownAsBanner />
|
||||
</Variant>
|
||||
</div>
|
||||
</Decision>
|
||||
<Sample label="after Létrehozás">
|
||||
<ShownInDialog />
|
||||
</Sample>
|
||||
</Reference>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user