A /watch link may now open one of its owner's own folders (migration 0108: folder_id beside job_id, exactly one set; a folder's links go with it). The page shows what is in the folder when it is opened, newest first: a video or a song plays in the same player a file link has, above the list, each keeping its own resume point; a document only downloads, never opens as a page. A file moved out stops opening through the link at its next request. Password, expiry and stream-only work as on a file link; the unfurl names the folder and counts its files, never their names. Source folders and the Hub cannot be shared. The folder menu gets "Copy link" and "Share with a link…" (the Share dialog's link half); the rail marks a folder a live link opens. Two fixes in the Share dialog on the way: revoking the last link no longer mints a new one at once, and the new-link form goes back to downloadable after making a link.
165 lines
6.8 KiB
TypeScript
165 lines
6.8 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-tokens" className="rounded-md px-1.5 py-0.5 text-accent state-layer">
|
|
Tokens
|
|
</a>
|
|
<a href="#open-files" className="rounded-md px-1.5 py-0.5 text-accent 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'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: A, A, A — the variants with the accent
|
|
edge (built; on the public list a picture-less row keeps its glyph above the length).
|
|
</p>
|
|
</header>
|
|
<LinksRound />
|
|
<header id="open-tokens" className="mt-6 scroll-mt-20">
|
|
<h2 className="text-xl font-bold">Decided · API tokens, a machine's key to Files</h2>
|
|
<p className="text-sm text-muted">
|
|
Settings → API tokens. Picked: A, A, A — the variants with the accent edge (built; the
|
|
expiry is a segmented control, DESIGN.md "A choice between a few values").
|
|
</p>
|
|
</header>
|
|
<TokensRound />
|
|
<header id="open-files" className="mt-6 scroll-mt-20">
|
|
<h2 className="text-xl font-bold">Open · Files, the one library</h2>
|
|
<p className="text-sm text-muted">
|
|
The page that replaces Downloads. The recommended variant has the accent edge.
|
|
</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'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>
|
|
);
|
|
}
|