The trial is over: the owner kept it. The trial wording goes from the tokens, the scheme list, the lab and DESIGN; B2's own-hue rule still binds any new scheme.
549 lines
21 KiB
TypeScript
549 lines
21 KiB
TypeScript
import clsx from "clsx";
|
||
import { useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
|
||
import {
|
||
BarChart3,
|
||
Bell,
|
||
Check,
|
||
Clapperboard,
|
||
Download,
|
||
Home,
|
||
ListVideo,
|
||
Lock,
|
||
MessageSquare,
|
||
PlayCircle,
|
||
Repeat,
|
||
Settings,
|
||
SlidersHorizontal,
|
||
Tv,
|
||
Users,
|
||
Youtube,
|
||
} from "lucide-react";
|
||
import { Btn } from "./batchA";
|
||
import { hexContrast, hueGap } from "../lib/colorMath";
|
||
import { Reference, Sample } from "./labKit";
|
||
import { FADE_ABOVE_PANEL, FADE_BELOW, FADE_BELOW_PANEL, LAYER } from "./labTokens";
|
||
import { Chip } from "../components/ui/Chip";
|
||
import { SURFACE } from "../components/ui/surfaces";
|
||
import { SelectTrigger } from "../components/ui/SelectTrigger";
|
||
import { SkeletonShapes } from "../components/ui/Skeleton";
|
||
import { Button } from "../components/ui/Button";
|
||
|
||
// E28 S2 · Batch B — surfaces, chrome and colour, DECIDED (owner, 2026-09-24): every recommendation,
|
||
// except B7, where the owner combined the soft tint + edge bar with optional labels. B1 (density) and
|
||
// B3 (state layers) render inside batch A's references. Only the chosen rendering of each stays.
|
||
|
||
const ico = "w-4 h-4";
|
||
|
||
// ── B2 · the scheme set ──────────────────────────────────────────────────────────────────────────
|
||
// A scheme that is not in index.css yet is rendered by scoping its tokens on a wrapper: every
|
||
// utility resolves var(--…) at the use site (`@theme inline`), so the tile paints its own accent.
|
||
|
||
type Pal = {
|
||
bg: string;
|
||
surface: string;
|
||
border: string;
|
||
fg: string;
|
||
muted: string;
|
||
accent: string;
|
||
accentFg: string;
|
||
};
|
||
type Mode = "dark" | "light";
|
||
type SchemeDef = { name: string; dark: Pal; light: Pal };
|
||
|
||
const STATUS: Record<Mode, Record<"danger" | "dangerFg" | "warn" | "success", string>> = {
|
||
dark: { danger: "#f87171", dangerFg: "#2a0a0a", warn: "#fbbf24", success: "#34d399" },
|
||
light: { danger: "#b91c1c", dangerFg: "#ffffff", warn: "#a14a07", success: "#047857" },
|
||
};
|
||
|
||
const pal = (
|
||
bg: string,
|
||
surface: string,
|
||
border: string,
|
||
fg: string,
|
||
muted: string,
|
||
accent: string,
|
||
accentFg: string,
|
||
): Pal => ({ bg, surface, border, fg, muted, accent, accentFg });
|
||
|
||
// A literal copy of index.css's scheme tokens, so the tiles render side by side (the live CSS
|
||
// switches only one scheme at a time). index.css is the source; theme.contract.test.ts holds it.
|
||
const SCHEMES: SchemeDef[] = [
|
||
{
|
||
name: "Midnight · blue",
|
||
dark: pal("#0b1020", "#121a2e", "#243049", "#e6e9f0", "#97a3c0", "#6d8cff", "#0b1020"),
|
||
light: pal("#f4f6fc", "#ffffff", "#dde3f0", "#1a2238", "#5b6685", "#3b5bdb", "#ffffff"),
|
||
},
|
||
{
|
||
name: "Forest · cyan",
|
||
dark: pal("#0a1512", "#0f1f1a", "#1f3a30", "#e6f0ec", "#90b1a4", "#22d3ee", "#04110d"),
|
||
light: pal("#f1f7f4", "#ffffff", "#d6e7e0", "#102a22", "#4d6b60", "#0e7490", "#ffffff"),
|
||
},
|
||
{
|
||
name: "Slate · fuchsia",
|
||
dark: pal("#15171c", "#1b1e25", "#2c313b", "#e7e9ee", "#9aa1ad", "#e879f9", "#1a0620"),
|
||
light: pal("#f6f7f9", "#ffffff", "#e3e6eb", "#1c1f26", "#5d636e", "#a21caf", "#ffffff"),
|
||
},
|
||
{
|
||
name: "Matrix · moss",
|
||
dark: pal("#0b0e0b", "#111510", "#2a3226", "#d9ded6", "#8f9a8a", "#86a05a", "#0c1208"),
|
||
light: pal("#f4f5f0", "#ffffff", "#dfe3d8", "#161a14", "#5a6352", "#556b2f", "#ffffff"),
|
||
},
|
||
{
|
||
name: "Fjord · steel",
|
||
dark: pal("#0d1225", "#121931", "#25506e", "#eef1f5", "#9fb0c4", "#5aa1cb", "#0d1225"),
|
||
light: pal("#f3f6fa", "#ffffff", "#c9d6e3", "#152232", "#4f6278", "#256b8f", "#ffffff"),
|
||
},
|
||
];
|
||
|
||
function Metric({ ok, children }: { ok: boolean; children: ReactNode }) {
|
||
return (
|
||
<span className={clsx("tabular-nums", ok ? "text-success" : "text-danger")}>{children}</span>
|
||
);
|
||
}
|
||
|
||
function SchemeTile({ s, mode }: { s: SchemeDef; mode: Mode }) {
|
||
const p = s[mode];
|
||
const st = STATUS[mode];
|
||
const style = {
|
||
"--bg": p.bg,
|
||
"--surface": p.surface,
|
||
"--card": p.surface,
|
||
"--border": p.border,
|
||
"--fg": p.fg,
|
||
"--muted": p.muted,
|
||
"--accent": p.accent,
|
||
"--accent-fg": p.accentFg,
|
||
"--danger": st.danger,
|
||
"--danger-fg": st.dangerFg,
|
||
"--warn": st.warn,
|
||
"--success": st.success,
|
||
} as CSSProperties;
|
||
const worst = (["danger", "warn", "success"] as const)
|
||
.map((k) => ({ k, g: Math.round(hueGap(p.accent, st[k])) }))
|
||
.reduce((a, b) => (b.g < a.g ? b : a));
|
||
const text = hexContrast(p.accent, p.bg);
|
||
const onFill = hexContrast(p.accentFg, p.accent);
|
||
return (
|
||
<div
|
||
style={style}
|
||
className="flex flex-col gap-2 rounded-lg border border-border bg-bg p-2.5 text-fg"
|
||
>
|
||
<span className="text-xs font-semibold">
|
||
{s.name} <span className="font-normal text-muted">· {mode}</span>
|
||
</span>
|
||
<div className="flex flex-wrap gap-1.5">
|
||
<span className="state-filled rounded-lg px-2.5 py-1 text-xs font-medium">Save</span>
|
||
<span className="state-danger rounded-lg px-2.5 py-1 text-xs font-medium">Delete</span>
|
||
</div>
|
||
<div className="flex flex-wrap gap-1.5 text-2xs">
|
||
<span className="chip px-2 py-0.5">Filter</span>
|
||
<span className="rounded-full bg-warn/6 px-2 py-0.5 text-warn">Retrying</span>
|
||
<span className="rounded-full bg-success/6 px-2 py-0.5 text-success">Done</span>
|
||
</div>
|
||
<span className="flex flex-col rounded bg-surface px-1.5 py-1 text-2xs">
|
||
<Metric ok={worst.g >= 30}>{`closest status: ${worst.k} ${worst.g}°`}</Metric>
|
||
<Metric ok={text >= 4.5}>{`accent as text ${text.toFixed(2)}:1`}</Metric>
|
||
<Metric ok={onFill >= 4.5}>{`button text ${onFill.toFixed(2)}:1`}</Metric>
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── B4 · the page-chrome band ────────────────────────────────────────────────────────────────────
|
||
function Frame({ h, label, children }: { h: string; label: string; children: ReactNode }) {
|
||
return (
|
||
<Sample label={label}>
|
||
<div className={clsx("relative overflow-hidden rounded-xl border border-border bg-bg", h)}>
|
||
<div className="h-full overflow-y-auto no-scrollbar">{children}</div>
|
||
</div>
|
||
</Sample>
|
||
);
|
||
}
|
||
|
||
function Cards() {
|
||
return (
|
||
<div className="grid grid-cols-3 gap-2 p-3">
|
||
{Array.from({ length: 12 }, (_, i) => (
|
||
<div key={i} className={`${SURFACE.card} flex flex-col gap-1 p-1.5`}>
|
||
<div className="aspect-video rounded-lg bg-linear-to-br from-accent/40 to-card" />
|
||
<span className="truncate text-2xs">Video title number {i + 1}</span>
|
||
<span className="text-3xs text-muted">Channel · 2 days ago</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const tab = (on: boolean) =>
|
||
clsx(
|
||
"rounded-lg px-2.5 py-1 text-xs",
|
||
on ? "bg-accent text-accent-fg" : clsx("text-muted hover:text-fg", LAYER),
|
||
);
|
||
|
||
function Tabs() {
|
||
return (
|
||
<div className="flex gap-1">
|
||
<span className={tab(true)}>Queue</span>
|
||
<span className={tab(false)}>Library</span>
|
||
<span className={tab(false)}>History</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Band({ collapsed }: { collapsed?: boolean }) {
|
||
return (
|
||
<div className={clsx("sticky top-0 z-base flex flex-col gap-2 bg-bg px-3 py-2", FADE_BELOW)}>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<Tabs />
|
||
{collapsed && (
|
||
<SelectTrigger
|
||
context="toolbar"
|
||
open={false}
|
||
icon={<SlidersHorizontal className="size-3.5 shrink-0 text-muted" />}
|
||
>
|
||
Filters · 3
|
||
</SelectTrigger>
|
||
)}
|
||
</div>
|
||
{!collapsed && (
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{[
|
||
["State", "Unwatched"],
|
||
["Date", "Any"],
|
||
["Sort", "Newest"],
|
||
].map(([k, v]) => (
|
||
<SelectTrigger key={k} context="toolbar" open={false} label={k}>
|
||
{v}
|
||
</SelectTrigger>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── B5 · surface per role ────────────────────────────────────────────────────────────────────────
|
||
function Surfaces() {
|
||
return (
|
||
<div className="flex flex-col gap-3 rounded-xl bg-linear-to-br from-accent/35 via-card to-bg p-3">
|
||
<div className={`${SURFACE.panel} flex flex-col gap-2 p-3`}>
|
||
<span className="text-sm font-semibold">Dialog · .glass r16</span>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div className={`${SURFACE.card} p-2 text-xs`}>Card · r12</div>
|
||
<div className={`${SURFACE.card} p-2 text-xs`}>Picker · r12</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-3">
|
||
<div className={`${SURFACE.menu} flex w-44 flex-col text-xs`}>
|
||
{["Watch later", "Add to playlist", "Copy link"].map((t) => (
|
||
<span key={t} className={clsx("flex h-7 items-center rounded-lg px-2", LAYER)}>
|
||
{t}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<span className={`${SURFACE.tooltip} px-2 py-1 text-2xs`}>Tooltip · .glass-menu r8</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── B6 · the public watch page ───────────────────────────────────────────────────────────────────
|
||
function WatchPage() {
|
||
return (
|
||
<div className="flex max-w-xl flex-col gap-3 rounded-xl bg-linear-to-br from-accent/25 via-bg to-bg p-3">
|
||
<div className="flex items-center gap-2 text-xs text-muted">
|
||
<PlayCircle className="h-4 w-4 text-accent" />
|
||
<span className="font-semibold text-fg">Siftlode</span>
|
||
</div>
|
||
<div className="grid aspect-video place-items-center rounded-xl border border-border bg-black text-2xs text-white/60">
|
||
the browser's own video controls
|
||
</div>
|
||
<div className={`${SURFACE.panel} flex flex-wrap items-start gap-2 p-3`}>
|
||
<div className="min-w-0 flex-1 basis-40">
|
||
<div className="text-sm font-semibold">Building a home lab on a budget</div>
|
||
<div className="text-xs text-muted">Techno Tim</div>
|
||
</div>
|
||
<span className="flex gap-1.5">
|
||
<Chip size="md" pressed icon={<Repeat className="size-3.5" />} className="font-medium">
|
||
Repeat
|
||
</Chip>
|
||
<Btn tone="glass" size="md" icon={<Download className={ico} />}>
|
||
Download
|
||
</Btn>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── B7 · the nav rail (the owner's combination) ──────────────────────────────────────────────────
|
||
// A soft tint + an edge bar for "you are here"; labels optional (a per-user Settings switch). Slim: the
|
||
// rail is 3.75rem wide in BOTH modes (labels add height, never width), so each module needs a short
|
||
// rail label that fits the item; the full name stays in the hover tooltip. Each rail reports whether
|
||
// all its labels fit, measured live (it re-measures on the lab's text-size slider).
|
||
|
||
type NavDef = { icon: typeof Home; en: string; hu: string };
|
||
const NAV: NavDef[] = [
|
||
{ icon: Home, en: "Home", hu: "Kezdőlap" },
|
||
{ icon: Youtube, en: "Feed", hu: "Feed" },
|
||
{ icon: Tv, en: "Channels", hu: "Csatornák" },
|
||
{ icon: ListVideo, en: "Playlists", hu: "Listák" },
|
||
{ icon: Clapperboard, en: "Plex", hu: "Plex" },
|
||
{ icon: Bell, en: "Alerts", hu: "Értesítés" },
|
||
{ icon: MessageSquare, en: "Messages", hu: "Üzenetek" },
|
||
{ icon: Download, en: "Downloads", hu: "Letöltések" },
|
||
];
|
||
const SYSTEM: NavDef[] = [
|
||
{ icon: BarChart3, en: "Stats", hu: "Statisztika" },
|
||
{ icon: Users, en: "Users", hu: "Fiókok" },
|
||
{ icon: Settings, en: "Settings", hu: "Beállítások" },
|
||
];
|
||
|
||
function RailItem({ d, on, lang }: { d: NavDef; on: boolean; lang?: "en" | "hu" }) {
|
||
const I = d.icon;
|
||
return (
|
||
<span
|
||
className={clsx(
|
||
"relative flex w-full flex-col items-center justify-center gap-0.5 rounded-lg",
|
||
lang ? "py-1.5" : "h-9",
|
||
on ? "bg-accent/15 text-accent" : clsx("text-muted hover:text-fg", LAYER),
|
||
)}
|
||
>
|
||
{on && (
|
||
<span className="absolute -left-1 top-1.5 bottom-1.5 w-0.75 rounded-r-full bg-accent" />
|
||
)}
|
||
<I className="h-4.5 w-4.5" />
|
||
{lang && (
|
||
<span
|
||
data-rail-label
|
||
className="max-w-full overflow-hidden whitespace-nowrap text-3xs font-medium tracking-tight"
|
||
>
|
||
{d[lang]}
|
||
</span>
|
||
)}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function Rail({ lang }: { lang?: "en" | "hu" }) {
|
||
const ref = useRef<HTMLDivElement>(null);
|
||
const [clipped, setClipped] = useState<string[]>([]);
|
||
useLayoutEffect(() => {
|
||
const measure = () =>
|
||
setClipped(
|
||
[...(ref.current?.querySelectorAll<HTMLElement>("[data-rail-label]") ?? [])]
|
||
.filter((e) => e.scrollWidth > e.clientWidth)
|
||
.map((e) => e.textContent ?? ""),
|
||
);
|
||
measure();
|
||
const mo = new MutationObserver(measure);
|
||
mo.observe(document.documentElement, { attributes: true });
|
||
return () => mo.disconnect();
|
||
}, []);
|
||
return (
|
||
<div className="flex flex-col items-start gap-1.5">
|
||
<div
|
||
ref={ref}
|
||
className={`${SURFACE.panel} flex w-15 flex-col items-center gap-0.5 px-1 py-2.5`}
|
||
>
|
||
{NAV.map((d, i) => (
|
||
<RailItem key={d.en} d={d} on={i === 1} lang={lang} />
|
||
))}
|
||
<span className="my-1.5 w-7 border-t-2 border-border" />
|
||
{SYSTEM.map((d) => (
|
||
<RailItem key={d.en} d={d} on={false} lang={lang} />
|
||
))}
|
||
</div>
|
||
{lang && (
|
||
<span className={clsx("text-2xs", clipped.length ? "text-danger" : "text-success")}>
|
||
{clipped.length ? `clipped: ${clipped.join(", ")}` : "every label fits ✓"}
|
||
</span>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── B8 · loading ─────────────────────────────────────────────────────────────────────────────────
|
||
function Loading() {
|
||
return (
|
||
<>
|
||
{(["grid", "poster", "list", "table"] as const).map((shape) => (
|
||
<Sample key={shape} label={`First load, ${shape}: its skeleton (after ~300ms)`}>
|
||
<div className="max-w-xl">
|
||
<SkeletonShapes shape={shape} />
|
||
</div>
|
||
</Sample>
|
||
))}
|
||
<Sample label="A control's own work: its spinner">
|
||
<Button loading>Saving</Button>
|
||
</Sample>
|
||
</>
|
||
);
|
||
}
|
||
|
||
// ── B9 · the fade edges (1–4 chosen) ─────────────────────────────────────────────────────────────
|
||
const LINES = Array.from({ length: 14 }, (_, i) => `Row ${i + 1} · Some content that scrolls`);
|
||
|
||
function ScrollBody() {
|
||
return (
|
||
<div className="flex flex-col gap-1 px-3 pb-3 pt-2 text-xs">
|
||
{LINES.map((l) => (
|
||
<span key={l} className="rounded-md bg-card/60 px-2 py-1.5">
|
||
{l}
|
||
</span>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const CHIPS = ["Unwatched", "Shorts", "Live", "Music", "Tech", "Science", "News", "Gaming"];
|
||
|
||
function Fades() {
|
||
return (
|
||
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]">
|
||
<Frame h="h-44" label="1 · Toolbars and table headers">
|
||
<div
|
||
className={clsx("sticky top-0 z-base bg-bg px-3 py-2 text-xs font-medium", FADE_BELOW)}
|
||
>
|
||
Title · Channel · Date
|
||
</div>
|
||
<ScrollBody />
|
||
</Frame>
|
||
<Sample label="2 · Tabs over a scrolling panel">
|
||
<div
|
||
className={`${SURFACE.panel} relative flex h-44 flex-col overflow-hidden panel-scroll-scope`}
|
||
>
|
||
<div className={clsx("relative z-base px-3 pb-2 pt-3", FADE_BELOW_PANEL)}>
|
||
<Tabs />
|
||
</div>
|
||
<div className="min-h-0 flex-1 overflow-y-auto no-scrollbar panel-scroller">
|
||
<ScrollBody />
|
||
</div>
|
||
</div>
|
||
</Sample>
|
||
<Sample label="3 · A dialog's header and footer">
|
||
<div
|
||
className={`${SURFACE.panel} relative flex h-44 flex-col overflow-hidden panel-scroll-scope`}
|
||
>
|
||
<div
|
||
className={clsx(
|
||
"relative z-base flex items-center gap-2 px-3 py-2 text-sm font-semibold",
|
||
FADE_BELOW_PANEL,
|
||
)}
|
||
>
|
||
<Lock className="h-4 w-4 text-accent" /> Edit details
|
||
</div>
|
||
<div className="min-h-0 flex-1 overflow-y-auto no-scrollbar panel-scroller">
|
||
<ScrollBody />
|
||
</div>
|
||
<div
|
||
className={clsx("relative z-base flex justify-end gap-2 px-3 py-2", FADE_ABOVE_PANEL)}
|
||
>
|
||
<Btn tone="ghost" size="md">
|
||
Cancel
|
||
</Btn>
|
||
<Btn tone="primary" size="md" icon={<Check className={ico} />}>
|
||
Save
|
||
</Btn>
|
||
</div>
|
||
</div>
|
||
</Sample>
|
||
<Sample label="4 · An overflowing chip row">
|
||
<div className="overflow-x-auto no-scrollbar fade-edges-x">
|
||
<div className="flex w-max gap-1.5 px-6">
|
||
{CHIPS.map((c) => (
|
||
<span key={c} className="chip px-2.5 py-1 text-xs">
|
||
{c}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Sample>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function BatchB() {
|
||
return (
|
||
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(20rem,1fr))]">
|
||
<Reference
|
||
id="b2"
|
||
code="B2"
|
||
title="The scheme set"
|
||
rule="Five schemes: four hue families — Midnight blue, Forest cyan, Slate fuchsia, Matrix moss — and Fjord, the one kept exception (Midnight's navy, Playnite's calm steel accent). YouTube and Starship are gone (a user on them lands on Midnight). Rule: an accent is ≥ 30° of OKLCH hue from danger, warn and success, and ≥ 4.5:1 as text and under its button text, dark and light."
|
||
>
|
||
<div className="grid gap-2 grid-cols-[repeat(auto-fill,minmax(9.5rem,1fr))]">
|
||
{SCHEMES.flatMap((s) =>
|
||
(["dark", "light"] as const).map((m) => <SchemeTile key={s.name + m} s={s} mode={m} />),
|
||
)}
|
||
</div>
|
||
</Reference>
|
||
<Reference
|
||
id="b4"
|
||
code="B4"
|
||
title="Page chrome: one sticky band"
|
||
rule="Every page's tabs and filters sit in one sticky band of the page colour, with the fade under it. Below ~600px of height the filter row folds into one 'Filters · n' button. Every page has a visually hidden h1."
|
||
>
|
||
<Frame h="h-64" label="Normal height · scroll it">
|
||
<Band />
|
||
<Cards />
|
||
</Frame>
|
||
<Frame h="h-36" label="Short (keyboard up)">
|
||
<Band collapsed />
|
||
<Cards />
|
||
</Frame>
|
||
</Reference>
|
||
<Reference
|
||
id="b5"
|
||
code="B5"
|
||
title="Surface per role"
|
||
rule="Dialog / panel .glass r16 · card and picker .glass-card r12 · menu .glass-menu r12, 4px padding, items r8 · tooltip .glass-menu r8. Translucency only from the tiers (app, Plex media, light), never per site."
|
||
>
|
||
<Surfaces />
|
||
</Reference>
|
||
<Reference
|
||
id="b6"
|
||
code="B6"
|
||
title="The public watch page"
|
||
rule="The app's surfaces (glass, Button, the pressed-pill toggle, the scheme's ambient backdrop); the video keeps the browser's own controls."
|
||
>
|
||
<WatchPage />
|
||
</Reference>
|
||
<Reference
|
||
id="b7"
|
||
code="B7"
|
||
title="The nav rail"
|
||
rule="A NavRail primitive. 'You are here' = a soft accent tint + a 3px edge bar. Labels under the icons are optional, a per-user switch in Settings (off by default). Slim: 3.75rem wide either way, labels add height only; each module gets a short rail label, the full name stays in the tooltip. 44 tall on touch (A3)."
|
||
>
|
||
<div className="flex flex-wrap items-start gap-6">
|
||
<Sample label="Icons only">
|
||
<Rail />
|
||
</Sample>
|
||
<Sample label="Labels · EN">
|
||
<Rail lang="en" />
|
||
</Sample>
|
||
<Sample label="Labels · HU">
|
||
<Rail lang="hu" />
|
||
</Sample>
|
||
</div>
|
||
</Reference>
|
||
<Reference
|
||
id="b8"
|
||
code="B8"
|
||
title="Loading"
|
||
rule="By shape: a Skeleton primitive for a first load of a grid, list or table; a spinner inside a control (the loading prop). Neither shows before ~300ms."
|
||
>
|
||
<Loading />
|
||
</Reference>
|
||
<Reference
|
||
id="b9"
|
||
code="B9"
|
||
title="The header + fade edge"
|
||
rule="On sticky toolbars and table headers, tabs over a scrolling panel, a long dialog's header and footer, and an overflowing chip row's clipped ends. Not on thumbnails, not on buttons."
|
||
>
|
||
<Fades />
|
||
</Reference>
|
||
</div>
|
||
);
|
||
}
|