feat(design): glass surfaces by role, the static Badge, LinkButton, user pickers on the Combobox (E28 S3d1)

This commit is contained in:
2026-09-25 00:34:35 +02:00
parent db10bfbbca
commit c47c193ad1
89 changed files with 818 additions and 478 deletions
+4 -1
View File
@@ -5,6 +5,7 @@ import { BatchB } from "./batchB";
import { BatchC } from "./batchC";
import { Chip } from "../components/ui/Chip";
import { Slider } from "../components/ui/NativeControls";
import { SURFACE } from "../components/ui/surfaces";
// The design lab (E28): 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 /
@@ -38,7 +39,9 @@ export default function DesignLab() {
return (
<div className="min-h-dvh text-fg" data-testid="design-lab">
<div className="glass sticky top-0 z-chrome flex flex-wrap items-center gap-x-4 gap-y-2 px-4 py-2.5">
<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) => (
+3 -5
View File
@@ -1,7 +1,6 @@
import clsx from "clsx";
import type { ReactNode } from "react";
import {
ChevronDown,
Download,
ListPlus,
MoreHorizontal,
@@ -72,10 +71,9 @@ function TouchRow() {
<span className={clsx(pill, "rounded-full bg-accent text-accent-fg")}>Mine</span>
<span className={clsx(pill, "rounded-full text-muted")}>Library</span>
</span>
<span className={clsx(pill, "glass-card gap-1.5 rounded-full border")}>
<span className="text-muted">Sort:</span> Newest
<ChevronDown className="h-3.5 w-3.5 text-muted" />
</span>
<SelectTrigger context="toolbar" open={false} label="Sort">
Newest
</SelectTrigger>
</div>
<div className="flex min-h-11 items-center justify-between rounded-lg bg-card/60 px-3">
<span className="text-sm">Dark mode</span>
+28 -23
View File
@@ -4,7 +4,6 @@ import {
BarChart3,
Bell,
Check,
ChevronDown,
Clapperboard,
Download,
Home,
@@ -25,6 +24,8 @@ import { hexContrast, hueGap } from "../lib/colorMath";
import { Reference, Sample } from "./labKit";
import { FADE_ABOVE_PANEL, FADE_BELOW, FADE_BELOW_PANEL, FILLED, H, LAYER } from "./labTokens";
import { Chip } from "../components/ui/Chip";
import { SURFACE } from "../components/ui/surfaces";
import { SelectTrigger } from "../components/ui/SelectTrigger";
// 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
@@ -157,7 +158,7 @@ function Cards() {
return (
<div className="grid grid-cols-3 gap-2 p-3">
{Array.from({ length: 12 }, (_, i) => (
<div key={i} className="glass-card flex flex-col gap-1 rounded-xl p-1.5">
<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>
@@ -172,10 +173,6 @@ const tab = (on: boolean) =>
"rounded-lg px-2.5 py-1 text-xs",
on ? "bg-accent text-accent-fg" : clsx("text-muted hover:text-fg", LAYER),
);
const pillCtl = clsx(
"glass-card inline-flex items-center gap-1 rounded-full border px-2.5 text-xs",
H.bar,
);
function Tabs() {
return (
@@ -193,17 +190,25 @@ function Band({ collapsed }: { collapsed?: boolean }) {
<div className="flex items-center justify-between gap-2">
<Tabs />
{collapsed && (
<span className={pillCtl}>
<SlidersHorizontal className="h-3 w-3" /> Filters · 3
</span>
<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((f) => (
<span key={f} className={pillCtl}>
{f} <ChevronDown className="h-3 w-3 text-muted" />
</span>
{[
["State", "Unwatched"],
["Date", "Any"],
["Sort", "Newest"],
].map(([k, v]) => (
<SelectTrigger key={k} context="toolbar" open={false} label={k}>
{v}
</SelectTrigger>
))}
</div>
)}
@@ -215,22 +220,22 @@ function Band({ collapsed }: { collapsed?: boolean }) {
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="glass flex flex-col gap-2 rounded-2xl 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="glass-card rounded-xl p-2 text-xs">Card · r12</div>
<div className="glass-card rounded-xl p-2 text-xs">Picker · r12</div>
<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="glass-menu flex w-44 flex-col rounded-xl p-1 text-xs">
<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="glass-menu rounded-lg px-2 py-1 text-2xs">Tooltip · .glass-menu r8</span>
<span className={`${SURFACE.tooltip} px-2 py-1 text-2xs`}>Tooltip · .glass-menu r8</span>
</div>
</div>
);
@@ -247,7 +252,7 @@ function WatchPage() {
<div className="grid aspect-video place-items-center rounded-xl border border-border bg-black text-2xs text-white/60">
the browser&apos;s own video controls
</div>
<div className="glass flex flex-wrap items-start gap-2 rounded-2xl p-3">
<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>
@@ -333,7 +338,7 @@ function Rail({ lang }: { lang?: "en" | "hu" }) {
<div className="flex flex-col items-start gap-1.5">
<div
ref={ref}
className="glass flex w-15 flex-col items-center gap-0.5 rounded-2xl px-1 py-2.5"
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} />
@@ -361,7 +366,7 @@ function Loading() {
<Sample label="A grid, first load: its skeleton (after ~300ms)">
<div className="grid max-w-md grid-cols-3 gap-2">
{Array.from({ length: 3 }, (_, i) => (
<div key={i} className="glass-card flex flex-col gap-1.5 rounded-xl p-1.5">
<div key={i} className={`${SURFACE.card} flex flex-col gap-1.5 p-1.5`}>
<div className={clsx("aspect-video rounded-lg", bone)} />
<div className={clsx("h-2 w-4/5", bone)} />
<div className={clsx("h-2 w-1/2", bone)} />
@@ -413,7 +418,7 @@ function Fades() {
<ScrollBody />
</Frame>
<Sample label="2 · Tabs over a scrolling panel">
<div className="glass relative flex h-44 flex-col overflow-hidden rounded-2xl">
<div className={`${SURFACE.panel} relative flex h-44 flex-col overflow-hidden`}>
<div className={clsx("relative z-base px-3 pb-2 pt-3", FADE_BELOW_PANEL)}>
<Tabs />
</div>
@@ -423,7 +428,7 @@ function Fades() {
</div>
</Sample>
<Sample label="3 · A dialog's header and footer">
<div className="glass relative flex h-44 flex-col overflow-hidden rounded-2xl">
<div className={`${SURFACE.panel} relative flex h-44 flex-col overflow-hidden`}>
<div
className={clsx(
"relative z-base flex items-center gap-2 px-3 py-2 text-sm font-semibold",
+8 -5
View File
@@ -24,6 +24,7 @@ import { Btn } from "./batchA";
import { C1 } from "./batchCActions";
import { Reference, Sample } from "./labKit";
import { FADE_ABOVE_PANEL, FADE_BELOW_PANEL, H, LAYER, STRONG } from "./labTokens";
import { SURFACE } from "../components/ui/surfaces";
// E28 S2 · Batch C — behaviour on touch and on a narrow window, DECIDED (owner, 2026-09-24): C1–C4 as
// proposed; C5 is the owner's reworked Plex control set (found by the live player pass, E28 S2d: the
@@ -45,7 +46,7 @@ function Content() {
return (
<div className="grid grid-cols-3 gap-2 p-3">
{Array.from({ length: 9 }, (_, i) => (
<div key={i} className="glass-card flex flex-col gap-1 rounded-xl p-1.5">
<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 {i + 1}</span>
</div>
@@ -56,7 +57,7 @@ function Content() {
function FilterPanel() {
return (
<div className="glass flex h-full w-44 flex-col gap-2 rounded-r-2xl p-3 text-xs">
<div className={`${SURFACE.panel} flex h-full w-44 flex-col gap-2 p-3 text-xs`}>
<div className="flex items-center justify-between">
<span className="font-semibold">Filters</span>
<span className="flex gap-0.5">
@@ -163,7 +164,7 @@ function C4() {
title="Dialog conventions"
rule="Modal takes an initialFocus by purpose (APG): a form focuses its first field, a confirm its least destructive button, anything else Close. The header and the footer are fixed, only the body scrolls, with the B9 fade at both edges, so Save stays one tap away with the keyboard up. Shown at a keyboard-up height."
>
<div className="glass relative flex h-52 flex-col overflow-hidden rounded-2xl">
<div className={`${SURFACE.panel} relative flex h-52 flex-col overflow-hidden`}>
<div
className={clsx(
"relative z-base flex items-center justify-between px-3 py-2 text-sm font-semibold",
@@ -270,7 +271,9 @@ function PlayerRow() {
<Maximize className="h-5 w-5" />
</span>
</div>
<div className="glass-menu absolute bottom-16 right-2 flex w-52 flex-col rounded-xl p-1 text-xs text-fg">
<div
className={`${SURFACE.menu} absolute bottom-16 right-2 flex w-52 flex-col text-xs text-fg`}
>
{MORE.map((c) => (
<span key={c.label} className="flex min-h-11 items-center gap-2.5 rounded-lg px-2.5">
<c.icon className={ico} />
@@ -286,7 +289,7 @@ function PlayerRow() {
function MediaCard({ state }: { state: "new" | "started" | "touch" }) {
const pill = "inline-flex h-8 items-center gap-1.5 rounded-full px-3 text-xs font-medium";
return (
<div className="glass-card flex w-44 flex-col gap-1.5 rounded-xl p-1.5">
<div className={`${SURFACE.card} flex w-44 flex-col gap-1.5 p-1.5`}>
<div className="relative aspect-2/3 overflow-hidden rounded-lg bg-linear-to-br from-accent/50 to-card">
{state !== "new" && (
<span className="absolute inset-x-0 bottom-0 h-1 bg-white/25">
+2 -1
View File
@@ -2,6 +2,7 @@ import clsx from "clsx";
import { Bookmark, Check, ListPlus, MoreHorizontal, Play } from "lucide-react";
import { Reference, Sample } from "./labKit";
import { H, LAYER } from "./labTokens";
import { SURFACE } from "../components/ui/surfaces";
// E28 S2 · Batch C, part 1 — action placement: the classification table (object × action → where it
// lives), DECIDED (owner, 2026-09-24: accepted as proposed). It is the source S3 builds each object's
@@ -316,7 +317,7 @@ function Card({ touch }: { touch: boolean }) {
touch ? "h-11 w-11" : H.iconSm,
);
return (
<div className="glass-card group flex w-60 flex-col gap-1.5 rounded-xl p-1.5">
<div className={`${SURFACE.card} group flex w-60 flex-col gap-1.5 p-1.5`}>
<div className="relative aspect-video overflow-hidden rounded-lg bg-linear-to-br from-accent/50 to-card">
<span className="absolute inset-x-0 bottom-0 h-1 bg-white/25">
<span className="block h-1 w-1/3 bg-accent" />
+2 -1
View File
@@ -1,5 +1,6 @@
import { useSyncExternalStore, type ReactNode } from "react";
import clsx from "clsx";
import { SURFACE } from "../components/ui/surfaces";
// Building blocks of the design lab. Every E28 decision is taken, so the lab is the reference half
// only: each <Reference> is the chosen rendering with the rule it became. A new design round brings
@@ -22,7 +23,7 @@ export function Reference({
children: ReactNode;
}) {
return (
<section id={id} className="glass-card rounded-xl p-4 flex flex-col gap-3 scroll-mt-20">
<section id={id} className={`${SURFACE.card} p-4 flex flex-col gap-3 scroll-mt-20`}>
<header>
<div className="flex items-baseline gap-3">
<span className="text-xs font-bold tracking-widest text-success">{code} ✓</span>