feat(design): C5 recorded, every E28 decision taken
The owner's lean Plex control set with both refinements (Back by position, a touch double-tap for the 10 s skip) goes into DESIGN.md's Players chapter and the log. The lab is now the reference only.
This commit is contained in:
+46
-35
@@ -4,9 +4,9 @@ This is the one written source of truth for the UI: principles, tokens, the cano
|
|||||||
and the interaction rules. Screens are measured against it. A pattern that has more than one look in the
|
and the interaction rules. Screens are measured against it. A pattern that has more than one look in the
|
||||||
code is either a decision still open here (marked **Pending**) or a violation to fix.
|
code is either a decision still open here (marked **Pending**) or a violation to fix.
|
||||||
|
|
||||||
**Status:** written in E28. Chapters marked _Decided_ are in force. One item is still **Pending** (C5, the
|
**Status:** written in E28; every chapter is _Decided_ and in force. The design lab (below) renders each rule's
|
||||||
Plex control row on a narrow screen), rendered side by side in the design lab (below) and settled by the
|
chosen form. A new question opens a **Pending** item here, with its variants in the lab, until the product
|
||||||
product owner.
|
owner settles it.
|
||||||
|
|
||||||
## How this document changes
|
## How this document changes
|
||||||
|
|
||||||
@@ -214,20 +214,30 @@ Wherever there is text, the user can select it and copy it, and no click action
|
|||||||
- **Motion:** 150–300ms micro-interactions, transform/opacity only; `prefers-reduced-motion` is respected.
|
- **Motion:** 150–300ms micro-interactions, transform/opacity only; `prefers-reduced-motion` is respected.
|
||||||
[WCAG 2.3.3]
|
[WCAG 2.3.3]
|
||||||
|
|
||||||
## Players — _Decided_ (C3), one item **Pending (C5)**
|
## Players — _Decided_ (C3, C5)
|
||||||
|
|
||||||
- **A tap on the Plex video only shows or hides the controls** (C3); play/pause is the button. The mouse keeps
|
- **A tap on the Plex video only shows or hides the controls** (C3); play/pause is the button. The mouse keeps
|
||||||
click-to-pause. Measured live: today a tap with the bar hidden both reveals it and pauses. [Nielsen #3]
|
click-to-pause. Measured live: before this, a tap with the bar hidden both revealed it and paused. [Nielsen #3]
|
||||||
- **The hide timer starts with playback,** not with the first touch (today, on touch, the bar stays up until
|
- **The hide timer starts with playback,** not with the first touch (before, on touch, the bar stayed up until
|
||||||
the first tap however long the video plays).
|
the first tap however long the video played).
|
||||||
- **The seek bar:** the track stays thin inside a 2.75rem (44px) hit area with `touch-action: none`, so a finger
|
- **The seek bar:** the track stays thin inside a 2.75rem (44px) hit area with `touch-action: none`, so a finger
|
||||||
drag scrubs instead of panning; the scrub head is always visible on touch. Controls are 44 on touch. [WCAG 2.5.8]
|
drag scrubs instead of panning; the scrub head is always visible on touch. Controls are 44 on touch. [WCAG 2.5.8]
|
||||||
|
- **The Plex control set (C5, the owner's model):** one row that fits a portrait tablet.
|
||||||
|
- No Stop (closing the player is stopping), no Restart, no ±10 s buttons.
|
||||||
|
- **Back:** more than 3 s in → back to the start; within the first 3 s → the previous item, at its resume
|
||||||
|
point if it has one, else its start. The first item has only the restart.
|
||||||
|
- **Next** shows only when there is a next item (an episode, a playlist queue). It starts that item at its
|
||||||
|
resume point and leaves one behind on the current item, which does not count as watched unless it is
|
||||||
|
already inside the finish margin (the credits).
|
||||||
|
- **±10 s:** the keyboard keeps ← / →; on touch a double-tap on the left or right third of the picture skips
|
||||||
|
∓10 s, while a single tap still only shows the controls. Touch never loses a function. [WCAG 2.5.1]
|
||||||
|
- The row: play/pause, Back, Next, mute, the time, audio & subtitles, a fixed **⋯** (player settings, media
|
||||||
|
info, stats, keyboard shortcuts, download the original file), fullscreen.
|
||||||
|
- **Restarting lives on the media card** (C5): not started → Play; started → **Resume** (primary) + **From
|
||||||
|
the start**. On touch a tap resumes and the card's "…" menu has From the start.
|
||||||
- **The YouTube modal:** its chrome uses the `Button` rungs (44 on touch), the side arrows stay inside the
|
- **The YouTube modal:** its chrome uses the `Button` rungs (44 on touch), the side arrows stay inside the
|
||||||
screen, and the description opens from an ⓘ button, because a hover popover never opens on a tap (measured
|
screen, and the description opens from an ⓘ button, because a hover popover never opens on a tap (measured
|
||||||
live). [WCAG 1.4.13]
|
live). [WCAG 1.4.13]
|
||||||
- **The control row on a narrow screen — Pending (C5):** today it runs off the right edge below 1024 (five
|
|
||||||
controls unreachable at 767 and at 820 portrait). Overflow into a "⋯ More" menu vs wrapping onto a second
|
|
||||||
line is open in the lab.
|
|
||||||
|
|
||||||
## Dialogs — _Decided_ (C4)
|
## Dialogs — _Decided_ (C4)
|
||||||
|
|
||||||
@@ -253,28 +263,29 @@ Wherever there is text, the user can select it and copy it, and no click action
|
|||||||
|
|
||||||
## Decision log
|
## Decision log
|
||||||
|
|
||||||
| Date | Decision | Chosen | Over | Why |
|
| Date | Decision | Chosen | Over | Why |
|
||||||
| ---------- | --------------------------- | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 2026-09-24 | Device contract | Tablet portrait + landscape and up; phones out | A 1024px floor with a "too small" message | Portrait tablet use is real, and the app already renders at 820 portrait |
|
| 2026-09-24 | Device contract | Tablet portrait + landscape and up; phones out | A 1024px floor with a "too small" message | Portrait tablet use is real, and the app already renders at 820 portrait |
|
||||||
| 2026-09-24 | Card actions on touch | Fewer visible, the rest in a "…" menu | 44px buttons that no longer fit the card | Six 44px targets need more width than a card has |
|
| 2026-09-24 | Card actions on touch | Fewer visible, the rest in a "…" menu | 44px buttons that no longer fit the card | Six 44px targets need more width than a card has |
|
||||||
| 2026-09-24 | Action placement | One menu with several triggers, never gesture-only, a desktop background menu that mirrors the toolbar | Visible buttons for everything | Fewer non-critical buttons on screen, without hiding any function |
|
| 2026-09-24 | Action placement | One menu with several triggers, never gesture-only, a desktop background menu that mirrors the toolbar | Visible buttons for everything | Fewer non-critical buttons on screen, without hiding any function |
|
||||||
| 2026-09-24 | Text selection | Text stays selectable; a click that ends a selection does nothing | Click-to-play on titles | The user must be able to copy any text the app shows |
|
| 2026-09-24 | Text selection | Text stays selectable; a click that ends a selection does nothing | Click-to-play on titles | The user must be able to copy any text the app shows |
|
||||||
| 2026-09-24 | A1 Button shape | r8 + a rung per context; glass is a Button variant | r12; a pill | Nested corners (r8 in r12 in r16); the pill already means "a filter or a choice" |
|
| 2026-09-24 | A1 Button shape | r8 + a rung per context; glass is a Button variant | r12; a pill | Nested corners (r8 in r12 in r16); the pill already means "a filter or a choice" |
|
||||||
| 2026-09-24 | A2 Icon buttons | Two sizes, r8; a round button only on media | One size; all round | A lone toolbar icon at the row size looks lost; round rows read as tags |
|
| 2026-09-24 | A2 Icon buttons | Two sizes, r8; a round button only on media | One size; all round | A lone toolbar icon at the row size looks lost; round rows read as tags |
|
||||||
| 2026-09-24 | A3 Touch size | 44 for every control primitive | 36 + gaps; Button only | One rule (HIG 44) instead of two sizes nobody remembers |
|
| 2026-09-24 | A3 Touch size | 44 for every control primitive | 36 + gaps; Button only | One rule (HIG 44) instead of two sizes nobody remembers |
|
||||||
| 2026-09-24 | A4 Input | Bordered, a 55% fg-into-bg edge (≥ 3:1) | Filled; today's look as a primitive | The only option that clears WCAG 1.4.11 without a heavy fill on glass |
|
| 2026-09-24 | A4 Input | Bordered, a 55% fg-into-bg edge (≥ 3:1) | Filled; today's look as a primitive | The only option that clears WCAG 1.4.11 without a heavy fill on glass |
|
||||||
| 2026-09-24 | A5 Dropdown trigger | Pill in a toolbar, r8 in a form | Pill everywhere; r8 everywhere | Matches the neighbours in each context |
|
| 2026-09-24 | A5 Dropdown trigger | Pill in a toolbar, r8 in a form | Pill everywhere; r8 everywhere | Matches the neighbours in each context |
|
||||||
| 2026-09-24 | A6 Checkbox, switch, slider | Native, accent-tinted, the row is the target | Drawn primitives | Free keyboard / screen-reader / forced-colours support; only 3 sites |
|
| 2026-09-24 | A6 Checkbox, switch, slider | Native, accent-tinted, the row is the target | Drawn primitives | Free keyboard / screen-reader / forced-colours support; only 3 sites |
|
||||||
| 2026-09-24 | B1 Density | Compact 32 / 28 / 24 by explicit height | Standard 38 / 32 / 26 | The owner's read of the lab; touch is covered separately (A3), and one toolbar row lines up at 28 |
|
| 2026-09-24 | B1 Density | Compact 32 / 28 / 24 by explicit height | Standard 38 / 32 / 26 | The owner's read of the lab; touch is covered separately (A3), and one toolbar row lines up at 28 |
|
||||||
| 2026-09-24 | B2 Scheme set | Midnight, Forest→cyan, Slate→fuchsia, Matrix→lime | Today's six | The accent collided with danger (YouTube), warn (Slate) and success (Forest, Matrix); Midnight and Starship were one scheme twice; 4 of 6 light accents failed 4.5:1 |
|
| 2026-09-24 | B2 Scheme set | Midnight, Forest→cyan, Slate→fuchsia, Matrix→lime | Today's six | The accent collided with danger (YouTube), warn (Slate) and success (Forest, Matrix); Midnight and Starship were one scheme twice; 4 of 6 light accents failed 4.5:1 |
|
||||||
| 2026-09-24 | B3 State layers | Text colour at 10% / 16% over anything | Material's 8 / 12%; today's opacity + bg-card | Visible in every scheme and mode (≥ 1.2:1); today's row hover was 1.00:1 on a card |
|
| 2026-09-24 | B3 State layers | Text colour at 10% / 16% over anything | Material's 8 / 12%; today's opacity + bg-card | Visible in every scheme and mode (≥ 1.2:1); today's row hover was 1.00:1 on a card |
|
||||||
| 2026-09-24 | B4 Page chrome | One sticky band + fade; filters fold when short | A floating glass bar; everything scrolls | Tabs and filters always in reach, in one place; a keyboard-up tablet keeps its content |
|
| 2026-09-24 | B4 Page chrome | One sticky band + fade; filters fold when short | A floating glass bar; everything scrolls | Tabs and filters always in reach, in one place; a keyboard-up tablet keeps its content |
|
||||||
| 2026-09-24 | B5 Surfaces | One class + radius per role | Today's per-site mix | Nested corners and no per-site alpha drift |
|
| 2026-09-24 | B5 Surfaces | One class + radius per role | Today's per-site mix | Nested corners and no per-site alpha drift |
|
||||||
| 2026-09-24 | B6 /watch | The app's surfaces, native video controls | Flat and minimal | The page strangers see should look like the product; native controls work on any device |
|
| 2026-09-24 | B6 /watch | The app's surfaces, native video controls | Flat and minimal | The page strangers see should look like the product; native controls work on any device |
|
||||||
| 2026-09-24 | B7 Nav rail | Soft tint + edge bar, optional labels (a user setting), slim | Solid accent (recommended); a Material rail | The owner's combination: calmer than a solid fill, labels for who wants them, no width cost |
|
| 2026-09-24 | B7 Nav rail | Soft tint + edge bar, optional labels (a user setting), slim | Solid accent (recommended); a Material rail | The owner's combination: calmer than a solid fill, labels for who wants them, no width cost |
|
||||||
| 2026-09-24 | B8 Loading | Skeleton by shape, spinner in a control | A spinner everywhere | Nothing jumps when data lands; a long wait doesn't look stuck |
|
| 2026-09-24 | B8 Loading | Skeleton by shape, spinner in a control | A spinner everywhere | Nothing jumps when data lands; a long wait doesn't look stuck |
|
||||||
| 2026-09-24 | B9 Fade | Toolbars / headers, panel tabs, dialog edges, chip rows | Thumbnails; buttons | The north-star depth where content scrolls under chrome; not on content or small controls |
|
| 2026-09-24 | B9 Fade | Toolbars / headers, panel tabs, dialog edges, chip rows | Thumbnails; buttons | The north-star depth where content scrolls under chrome; not on content or small controls |
|
||||||
| 2026-09-24 | C1 Action classification | The table (lab C1): state + primary at rest, everyday toggles on hover, "…" on touch, destructive into the menu | Visible buttons per site, as today | One action list per object, nothing gesture-only, fewer buttons without losing a function |
|
| 2026-09-24 | C1 Action classification | The table (lab C1): state + primary at rest, everyday toggles on hover, "…" on touch, destructive into the menu | Visible buttons per site, as today | One action list per object, nothing gesture-only, fewer buttons without losing a function |
|
||||||
| 2026-09-24 | C2 Side panels | A tap opens an overlay drawer; pin is a separate button ≥ 1024 | A tap pins and the content reflows | Below 768 the filters were unreachable; on touch a tap pinned and pushed the grid |
|
| 2026-09-24 | C2 Side panels | A tap opens an overlay drawer; pin is a separate button ≥ 1024 | A tap pins and the content reflows | Below 768 the filters were unreachable; on touch a tap pinned and pushed the grid |
|
||||||
| 2026-09-24 | C3 Players on touch | Tap = show the controls; 44px seek hit area; 44 buttons; the YT description behind ⓘ | Today (tap also pauses; 7.5px seek bar) | Measured live: you couldn't reach the seek bar without pausing, and the hover-only description never opened on a tap |
|
| 2026-09-24 | C3 Players on touch | Tap = show the controls; 44px seek hit area; 44 buttons; the YT description behind ⓘ | Today (tap also pauses; 7.5px seek bar) | Measured live: you couldn't reach the seek bar without pausing, and the hover-only description never opened on a tap |
|
||||||
| 2026-09-24 | C4 Dialogs | Focus by purpose; a pinned footer | Focus on Close; the footer scrolls | A form starts in its first field, and Save stays on screen with the keyboard up |
|
| 2026-09-24 | C4 Dialogs | Focus by purpose; a pinned footer | Focus on Close; the footer scrolls | A form starts in its first field, and Save stays on screen with the keyboard up |
|
||||||
|
| 2026-09-24 | C5 Plex player controls | The owner's lean set: no Stop / Restart / ±10 s buttons, Back by position, Next only with a next item, Resume + From the start on the card, touch double-tap ±10 s | Every control in one row (it ran off-screen below 1024); Back by a press counter; ±10 s keyboard-only | The row fits a portrait tablet; the decision to restart is made on the card; a tablet keeps the 10 s skip |
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import clsx from "clsx";
|
|||||||
import { SCHEMES, type Scheme } from "../lib/theme";
|
import { SCHEMES, type Scheme } from "../lib/theme";
|
||||||
import { BatchA } from "./batchA";
|
import { BatchA } from "./batchA";
|
||||||
import { BatchB } from "./batchB";
|
import { BatchB } from "./batchB";
|
||||||
import { BatchC, C5 } from "./batchC";
|
import { BatchC } from "./batchC";
|
||||||
|
|
||||||
// The open decisions, in the order they are asked. The decided ones render below them as reference.
|
|
||||||
const OPEN = [{ id: "c5", code: "C5", title: "Player controls", render: C5 }] as const;
|
|
||||||
|
|
||||||
// The design lab (E28): the living style guide. Dev-only — see main.tsx and devOnlyGuard.ts. It sits
|
// 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 /
|
// OUTSIDE <App>, so it owns the theme attributes on <html> itself, through the same data-theme /
|
||||||
@@ -93,15 +90,6 @@ export default function DesignLab() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<nav className="flex flex-wrap gap-1 text-xs">
|
<nav className="flex flex-wrap gap-1 text-xs">
|
||||||
{OPEN.map((d) => (
|
|
||||||
<a
|
|
||||||
key={d.id}
|
|
||||||
href={`#${d.id}`}
|
|
||||||
className="rounded-md px-1.5 py-0.5 text-muted hover:bg-card hover:text-fg"
|
|
||||||
>
|
|
||||||
{d.code} {d.title}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
<a href="#decided" className="rounded-md px-1.5 py-0.5 text-success hover:bg-card">
|
<a href="#decided" className="rounded-md px-1.5 py-0.5 text-success hover:bg-card">
|
||||||
A ✓
|
A ✓
|
||||||
</a>
|
</a>
|
||||||
@@ -116,15 +104,12 @@ export default function DesignLab() {
|
|||||||
|
|
||||||
<main className="mx-auto flex max-w-screen-2xl flex-col gap-6 p-4">
|
<main className="mx-auto flex max-w-screen-2xl flex-col gap-6 p-4">
|
||||||
<header>
|
<header>
|
||||||
<h1 className="text-2xl font-bold">Open · the Plex player controls</h1>
|
<h1 className="text-2xl font-bold">The design reference</h1>
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
Each decision shows its variants with the app's real CSS. The framed one is the
|
Every rule of docs/DESIGN.md in its chosen rendering, with the app's real CSS.
|
||||||
recommendation. Resize the window to tablet size to see them there too.
|
Resize the window to tablet size to see them there too.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
{OPEN.map((d) => (
|
|
||||||
<d.render key={d.id} />
|
|
||||||
))}
|
|
||||||
<header id="decided" className="mt-6 scroll-mt-20">
|
<header id="decided" className="mt-6 scroll-mt-20">
|
||||||
<h2 className="text-xl font-bold">Decided · batch A, controls</h2>
|
<h2 className="text-xl font-bold">Decided · batch A, controls</h2>
|
||||||
<p className="text-sm text-muted">
|
<p className="text-sm text-muted">
|
||||||
|
|||||||
@@ -22,13 +22,12 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Btn } from "./batchA";
|
import { Btn } from "./batchA";
|
||||||
import { C1 } from "./batchCActions";
|
import { C1 } from "./batchCActions";
|
||||||
import { Decision, Reference, Sample, Variant } from "./labKit";
|
import { Reference, Sample } from "./labKit";
|
||||||
import { FADE_ABOVE_PANEL, FADE_BELOW_PANEL, H, LAYER, STRONG } from "./labTokens";
|
import { FADE_ABOVE_PANEL, FADE_BELOW_PANEL, H, LAYER, STRONG } from "./labTokens";
|
||||||
|
|
||||||
// E28 S2 · Batch C — behaviour on touch and on a narrow window. C1–C4 are DECIDED (owner, 2026-09-24:
|
// E28 S2 · Batch C — behaviour on touch and on a narrow window, DECIDED (owner, 2026-09-24): C1–C4 as
|
||||||
// all as proposed) and stay as references. C5 is open: the live player pass (E28 S2d) found the Plex
|
// proposed; C5 is the owner's reworked Plex control set (found by the live player pass, E28 S2d: the
|
||||||
// control row running off-screen below 1024; the owner answered with a reworked control set, and
|
// old row ran off-screen below 1024) with the two refinements they accepted. References only.
|
||||||
// two of its points are asked back.
|
|
||||||
|
|
||||||
const ico = "w-4 h-4";
|
const ico = "w-4 h-4";
|
||||||
|
|
||||||
@@ -206,14 +205,14 @@ export function BatchC() {
|
|||||||
<C3 />
|
<C3 />
|
||||||
<C4 />
|
<C4 />
|
||||||
</div>
|
</div>
|
||||||
|
<C5 />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── C5 · the Plex player's controls, reworked (the owner's model, 2026-09-24) ─────────────────────
|
// ── C5 · the Plex player's controls (the owner's model, 2026-09-24) ──────────────────────────────
|
||||||
// The owner's direction: Stop, Restart and ±10 s leave the row; Back restarts or steps to the previous
|
// What is left after Stop, Restart and ±10 s leave the row fits a portrait tablet in one row, with a
|
||||||
// episode; Next exists only when there is a next item; restarting moves onto the card. What is left
|
// fixed "⋯" for the rare tools; restarting moves onto the card.
|
||||||
// fits a portrait tablet in one row, with a fixed "⋯" for the rare tools.
|
|
||||||
|
|
||||||
const ROW: { icon: typeof Pause; label: string }[] = [
|
const ROW: { icon: typeof Pause; label: string }[] = [
|
||||||
{ icon: Pause, label: "Play / pause" },
|
{ icon: Pause, label: "Play / pause" },
|
||||||
@@ -231,24 +230,22 @@ const MORE: { icon: typeof Pause; label: string }[] = [
|
|||||||
|
|
||||||
const ctl = "grid h-11 w-11 shrink-0 place-items-center rounded-full";
|
const ctl = "grid h-11 w-11 shrink-0 place-items-center rounded-full";
|
||||||
|
|
||||||
function PlayerRow({ touchZones }: { touchZones: boolean }) {
|
function PlayerRow() {
|
||||||
return (
|
return (
|
||||||
<div className="relative aspect-4/3 w-full overflow-hidden rounded-xl bg-linear-to-br from-slate-700 to-black text-white">
|
<div className="relative aspect-4/3 w-full overflow-hidden rounded-xl bg-linear-to-br from-slate-700 to-black text-white">
|
||||||
{touchZones && (
|
{/* Double-tap zones: the left and right thirds of the picture skip ∓10 s on touch. */}
|
||||||
// Double-tap zones: the left and right thirds of the picture skip ∓10 s on touch.
|
<div className="absolute inset-x-0 top-0 bottom-28 flex justify-between text-2xs text-white/70">
|
||||||
<div className="absolute inset-x-0 top-0 bottom-28 flex justify-between text-2xs text-white/70">
|
<span className="flex w-1/3 items-start justify-center border-r border-dashed border-white/25 pt-6">
|
||||||
<span className="flex w-1/3 items-start justify-center border-r border-dashed border-white/25 pt-6">
|
<span className="flex items-center gap-1">
|
||||||
<span className="flex items-center gap-1">
|
<Rewind className="h-4 w-4" /> double-tap −10 s
|
||||||
<Rewind className="h-4 w-4" /> double-tap −10 s
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span className="flex w-1/3 items-start justify-center border-l border-dashed border-white/25 pt-6">
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex w-1/3 items-start justify-center border-l border-dashed border-white/25 pt-6">
|
||||||
double-tap +10 s <FastForward className="h-4 w-4" />
|
<span className="flex items-center gap-1">
|
||||||
</span>
|
double-tap +10 s <FastForward className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</span>
|
||||||
)}
|
</div>
|
||||||
<div className="absolute inset-x-0 bottom-0 flex flex-col gap-1 bg-linear-to-t from-black/85 to-transparent px-2 pb-2 pt-10">
|
<div className="absolute inset-x-0 bottom-0 flex flex-col gap-1 bg-linear-to-t from-black/85 to-transparent px-2 pb-2 pt-10">
|
||||||
<div className="relative flex h-11 items-center">
|
<div className="relative flex h-11 items-center">
|
||||||
<div className="h-1 w-full rounded-full bg-white/30">
|
<div className="h-1 w-full rounded-full bg-white/30">
|
||||||
@@ -352,31 +349,20 @@ function Cards() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function C5() {
|
function C5() {
|
||||||
return (
|
return (
|
||||||
<Decision
|
<Reference
|
||||||
id="c5"
|
id="c5"
|
||||||
code="C5"
|
code="C5"
|
||||||
title="The Plex player's controls, reworked"
|
title="The Plex player's controls"
|
||||||
question="Your model: no Stop (closing is stopping), no Restart and no ±10 s buttons, Back restarts or goes to the previous episode, Next only where there is a next item, and restarting moves onto the card (Play when not started; Resume + From the start when started). It fits a 767 portrait tablet in one row, with a fixed '⋯'. Two points need your call: how Back tells 'restart' from 'previous', and how a tablet skips 10 s without a keyboard."
|
rule="The owner's model. No Stop (closing is stopping), no Restart and no ±10 s buttons. Back: more than 3 s in → restart; within the first 3 s → the previous item, at its resume point if it has one. Next shows only when there is a next item (an episode, a playlist), starts it at its resume point, and leaves one behind (not watched, unless already in the credits). On touch a double-tap on the left / right third skips ∓10 s; the keyboard keeps ← / →. The rare tools sit in a fixed '⋯'. Restarting lives on the card: not started → Play; started → Resume + From the start (touch: a tap resumes, the '…' has From the start)."
|
||||||
now="15 controls plus the time in one row that neither wraps nor scrolls: at 767 and at 820 portrait five of them are off-screen and unreachable. The card shows one Play ring; Resume and 'Clear resume point' exist only on the detail page."
|
|
||||||
>
|
>
|
||||||
<Variant
|
<div className="flex flex-wrap items-start gap-4">
|
||||||
letter="A"
|
<div className="w-full max-w-xl">
|
||||||
name="Your model, with two refinements"
|
<PlayerRow />
|
||||||
recommended
|
</div>
|
||||||
why="1) Back decides by the position, the music-player rule: more than 3 s in → restart; within the first 3 s → the previous episode (at its resume point if it has one, else its start). That is your 'first press restarts, the second goes back', without a press counter: a counter goes wrong when you arrive at an episode by Next and want the one before it at once (the first press would restart an episode already at 0:00). 2) On touch, a double-tap on the left or right third of the picture skips ∓10 s (the YouTube and Netflix gesture); a single tap still only shows the controls (C3). Without it a tablet can't go back 10 s at all: on a 42-minute film one pixel of the seek bar is ≈ 3.5 s, and a finger lands within 10–20 px, so ±40–70 s."
|
|
||||||
>
|
|
||||||
<PlayerRow touchZones />
|
|
||||||
<Cards />
|
<Cards />
|
||||||
</Variant>
|
</div>
|
||||||
<Variant
|
</Reference>
|
||||||
letter="B"
|
|
||||||
name="Your model, literally"
|
|
||||||
why="Back counts presses: the first restarts, a second one goes to the previous episode. ±10 s only from the keyboard (← / →), so on a tablet the seek bar is the only way to move, at a precision of tens of seconds. The row and the cards are the same as A."
|
|
||||||
>
|
|
||||||
<PlayerRow touchZones={false} />
|
|
||||||
</Variant>
|
|
||||||
</Decision>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +1,13 @@
|
|||||||
import { useSyncExternalStore, type ReactNode } from "react";
|
import { useSyncExternalStore, type ReactNode } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
// Building blocks of the design lab. A lab page asks the owner ONE question per <Decision>: two or
|
// Building blocks of the design lab. Every E28 decision is taken, so the lab is the reference half
|
||||||
// three <Variant>s rendered side by side with the app's real CSS, the recommended one marked and its
|
// only: each <Reference> is the chosen rendering with the rule it became. A new design round brings
|
||||||
// one-line reason (the rule it rests on) printed under it. Everything is static markup on purpose —
|
// back the side-by-side <Decision>/<Variant> pair (E28 S2 history) for its open questions. Everything
|
||||||
// what is being decided is the look, and a variant that only exists here must not need app state.
|
// is static markup on purpose: a rendering that only exists here must not need app state.
|
||||||
|
|
||||||
export function Decision({
|
|
||||||
id,
|
|
||||||
code,
|
|
||||||
title,
|
|
||||||
question,
|
|
||||||
now,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
id: string;
|
|
||||||
code: string;
|
|
||||||
title: string;
|
|
||||||
question: string;
|
|
||||||
/** What the app does today, measured (the E28 S1 inventory). */
|
|
||||||
now: ReactNode;
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<section id={id} className="glass rounded-2xl p-5 scroll-mt-20">
|
|
||||||
<header className="mb-4">
|
|
||||||
<div className="flex items-baseline gap-3">
|
|
||||||
<span className="text-xs font-bold tracking-widest text-accent">{code}</span>
|
|
||||||
<h2 className="text-lg font-bold">{title}</h2>
|
|
||||||
</div>
|
|
||||||
<p className="mt-1 text-sm">{question}</p>
|
|
||||||
<p className="mt-1 text-xs text-muted">
|
|
||||||
<span className="font-semibold">Today: </span>
|
|
||||||
{now}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(18rem,1fr))]">{children}</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Variant({
|
|
||||||
letter,
|
|
||||||
name,
|
|
||||||
recommended = false,
|
|
||||||
why,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
letter: string;
|
|
||||||
name: string;
|
|
||||||
recommended?: boolean;
|
|
||||||
why: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className={clsx(
|
|
||||||
"glass-card rounded-xl p-4 flex flex-col gap-3 border",
|
|
||||||
recommended ? "border-accent" : "border-transparent",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="grid h-6 w-6 place-items-center rounded-full bg-card text-xs font-bold">
|
|
||||||
{letter}
|
|
||||||
</span>
|
|
||||||
<h3 className="text-sm font-semibold flex-1">{name}</h3>
|
|
||||||
{recommended && (
|
|
||||||
<span className="rounded-full bg-accent px-2 py-0.5 text-2xs font-bold text-accent-fg">
|
|
||||||
Recommended
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-3">{children}</div>
|
|
||||||
<p className="mt-auto border-t border-border/60 pt-2 text-xs text-muted">{why}</p>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A decision already taken: the chosen rendering only, with the rule it became. The lab's
|
/** A decision already taken: the chosen rendering only, with the rule it became. The lab's
|
||||||
* reference half — the living style guide the open decisions sit on top of. */
|
* reference half — the living style guide. */
|
||||||
export function Reference({
|
export function Reference({
|
||||||
id,
|
id,
|
||||||
code,
|
code,
|
||||||
|
|||||||
Reference in New Issue
Block a user