Files
siftlode/frontend/src/design-lab/DesignLab.tsx
T
peter 2efa7d5a75 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.
2026-10-15 04:58:17 +02:00

170 lines
7.0 KiB
TypeScript

import { useLayoutEffect, useState } from "react";
import { SCHEMES, type Scheme } from "../lib/theme";
import { BatchA } from "./batchA";
import { BatchB } from "./batchB";
import { BatchC } from "./batchC";
import { BatchD } from "./batchD";
import { FilesRound } from "./filesRound";
import { TokensRound } from "./tokensRound";
import { LinksRound } from "./linksRound";
import { Chip } from "../components/ui/Chip";
import { Slider } from "../components/ui/NativeControls";
import { SURFACE } from "../components/ui/surfaces";
// The design lab: the living style guide. Dev-only — see main.tsx and devOnlyGuard.ts. It sits
// OUTSIDE <App>, so it owns the theme attributes on <html> itself, through the same data-theme /
// data-scheme / --font-scale / data-backdrop switches the app's PrefsProvider drives. Nothing here
// is saved: the lab's bar changes this tab only.
type Look = { mode: "dark" | "light"; scheme: Scheme; fontScale: number; backdrop: boolean };
// The owner's own daily look (as inventoried) is the starting point — decisions are taken on it.
const START: Look = { mode: "dark", scheme: "midnight", fontScale: 1.02, backdrop: true };
function applyLook(l: Look) {
const el = document.documentElement;
el.dataset.theme = l.mode;
el.dataset.scheme = l.scheme;
el.dataset.backdrop = l.backdrop ? "on" : "off";
el.style.setProperty("--font-scale", String(l.fontScale));
}
export default function DesignLab() {
const [look, setLook] = useState<Look>(START);
useLayoutEffect(() => {
applyLook(START);
document.title = "Design lab · Siftlode";
}, []);
const set = (patch: Partial<Look>) => {
const next = { ...look, ...patch };
applyLook(next);
setLook(next);
};
return (
<div className="min-h-dvh text-fg" data-testid="design-lab">
<div
className={`${SURFACE.band} sticky top-0 z-chrome flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-2.5`}
>
<strong className="text-sm">Design lab</strong>
<span className="flex gap-1">
{(["dark", "light"] as const).map((m) => (
<Chip key={m} pressed={look.mode === m} onClick={() => set({ mode: m })}>
{m}
</Chip>
))}
</span>
<span className="flex flex-wrap gap-1">
{SCHEMES.map((s) => (
<Chip key={s.id} pressed={look.scheme === s.id} onClick={() => set({ scheme: s.id })}>
<span className="h-2.5 w-2.5 rounded-full" style={{ background: s.swatch }} />
{s.name}
</Chip>
))}
</span>
<Chip pressed={look.backdrop} onClick={() => set({ backdrop: !look.backdrop })}>
backdrop
</Chip>
<label className="flex items-center gap-2 text-xs text-muted">
text {look.fontScale.toFixed(2)}
<Slider
className="w-24"
min={0.9}
max={1.2}
step={0.02}
value={look.fontScale}
onChange={(v) => set({ fontScale: v })}
/>
</label>
<nav className="flex flex-wrap gap-1 text-xs">
<a href="#open-links" className="rounded-md px-1.5 py-0.5 text-success state-layer">
Links ✓
</a>
<a href="#open-tokens" className="rounded-md px-1.5 py-0.5 text-success state-layer">
Tokens ✓
</a>
<a href="#open-files" className="rounded-md px-1.5 py-0.5 text-success state-layer">
Files ✓
</a>
<a href="#decided" className="rounded-md px-1.5 py-0.5 text-success state-layer">
A ✓
</a>
<a href="#decided-b" className="rounded-md px-1.5 py-0.5 text-success state-layer">
B ✓
</a>
<a href="#decided-c" className="rounded-md px-1.5 py-0.5 text-success state-layer">
C ✓
</a>
<a href="#decided-s3h" className="rounded-md px-1.5 py-0.5 text-success state-layer">
S3h ✓
</a>
</nav>
</div>
<main className="mx-auto flex max-w-screen-2xl flex-col gap-6 p-4">
<header>
<h1 className="text-2xl font-bold">The design reference</h1>
<p className="text-sm text-muted">
Every rule of docs/DESIGN.md in its chosen rendering, with the app&apos;s real CSS.
Resize the window to tablet size to see them there too.
</p>
</header>
<header id="open-links" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · Folder links</h2>
<p className="text-sm text-muted">
A /watch link that opens a whole folder. Picked: Copy link beside Share with link… in
the folder menu, a list the link opens with the player above it, a link mark in the
rail.
</p>
</header>
<LinksRound />
<header id="open-tokens" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · API tokens, a machine&apos;s key to Files</h2>
<p className="text-sm text-muted">
Settings → API tokens. Picked: a row per token in three lines, the kind asked first (Hub
csere or Egyedi, rights as chips per folder), the token shown once in the same dialog.
</p>
</header>
<TokensRound />
<header id="open-files" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · Files, the one library</h2>
<p className="text-sm text-muted">
The page that replaced Downloads. Picked: list rows (the grid a toggle), an app-wide
download tray, a done notification that copies the link.
</p>
</header>
<FilesRound />
<header id="decided" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · batch A, controls</h2>
<p className="text-sm text-muted">
The chosen rendering of each decision, the reference the primitives are built to.
</p>
</header>
<BatchA />
<header id="decided-b" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · batch B, surfaces, chrome and colour</h2>
<p className="text-sm text-muted">
B1 (density) and B3 (state layers) are rendered inside the batch A references above.
</p>
</header>
<BatchB />
<header id="decided-c" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · batch C, behaviour and device</h2>
<p className="text-sm text-muted">
C1 is the action classification each object&apos;s one action list is built from.
</p>
</header>
<BatchC />
<header id="decided-s3h" className="mt-6 scroll-mt-20">
<h2 className="text-xl font-bold">Decided · S3h, the media card</h2>
<p className="text-sm text-muted">
Hover a card: its Play and its hover row. The preview, the hero and the info page live
in the app.
</p>
</header>
<BatchD />
</main>
</div>
);
}