feat: uniform control height, chrome auto-hide, streaming quality
Every control in the app now shares one height (CONTROL_H), with width still growing to fit its label. The player transport is larger, Back is an icon, and the footer buttons match the prev/next size. Player chrome — transport and edge arrows — fades after 2.6s of inactivity and never hides while paused. The title-bar strip collapses in macOS window fullscreen, where the traffic lights are gone and it was just a blank white bar. Streaming quality is now selectable alongside download quality. A cap is applied by rewriting YouTube's HLS master playlist down to the best variant at or below the chosen height, keeping its audio group. That playlist is served from a small loopback HTTP server: Safari's native HLS is backed by AVFoundation, which cannot read blob: or custom-scheme URLs, so a Blob URL silently fails to play. Settings closes with an icon button and its selects match the shared control height.
This commit is contained in:
+22
-9
@@ -4,6 +4,17 @@
|
||||
*/
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Every control in the app is this tall. Width still grows with the label —
|
||||
* only the height is fixed, so a row of mixed buttons lines up.
|
||||
*/
|
||||
export const CONTROL_H = "h-[30px]";
|
||||
|
||||
/** Square version, for icon-only buttons. */
|
||||
export const ICON_BTN =
|
||||
`grid ${CONTROL_H} w-[30px] shrink-0 place-items-center rounded-lg cursor-pointer ` +
|
||||
"disabled:cursor-not-allowed disabled:opacity-40";
|
||||
|
||||
export const HEADING =
|
||||
"text-[11px] font-bold uppercase tracking-widest text-slate-500 dark:text-slate-400";
|
||||
export const LABEL = "text-[12px] text-slate-500 dark:text-slate-400";
|
||||
@@ -14,27 +25,29 @@ export const PANEL =
|
||||
export const SECTION =
|
||||
"border-b border-slate-200 px-4 py-4 dark:border-slate-800";
|
||||
export const INPUT =
|
||||
"w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-[13px] outline-none " +
|
||||
`w-full ${CONTROL_H} rounded-lg border border-slate-300 bg-white px-3 text-[12px] outline-none ` +
|
||||
"placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:placeholder:text-slate-500";
|
||||
export const SUBPANEL = "rounded-lg bg-slate-50 p-3 dark:bg-slate-800/50";
|
||||
|
||||
const BTN_BASE = "rounded-lg text-[13px] disabled:cursor-not-allowed";
|
||||
const BTN_BASE =
|
||||
`inline-flex ${CONTROL_H} items-center justify-center rounded-lg text-[12px] ` +
|
||||
"disabled:cursor-not-allowed";
|
||||
|
||||
export const BTN =
|
||||
`${BTN_BASE} border border-slate-300 px-3 py-2 font-medium ` +
|
||||
`${BTN_BASE} border border-slate-300 px-2.5 font-medium ` +
|
||||
"hover:border-sky-500 hover:text-sky-600 disabled:opacity-40 " +
|
||||
"dark:border-slate-700 dark:hover:border-sky-500 dark:hover:text-sky-400";
|
||||
|
||||
export const BTN_PRIMARY =
|
||||
`${BTN_BASE} bg-sky-500 px-3 py-2 font-semibold text-white ` +
|
||||
`${BTN_BASE} bg-sky-500 px-3 font-semibold text-white ` +
|
||||
"hover:bg-sky-400 disabled:opacity-40";
|
||||
|
||||
export const BTN_DANGER =
|
||||
`${BTN_BASE} bg-red-600 px-3 py-1.5 font-semibold text-white hover:bg-red-500`;
|
||||
`${BTN_BASE} bg-red-600 px-3 font-semibold text-white hover:bg-red-500`;
|
||||
|
||||
/** Header actions: quieter than a secondary button, still a real target. */
|
||||
export const BTN_CHROME =
|
||||
"rounded-md px-2 py-1 text-[11px] font-medium text-slate-500 " +
|
||||
`inline-flex ${CONTROL_H} items-center rounded-lg px-2 text-[12px] font-medium text-slate-500 ` +
|
||||
"hover:bg-slate-100 hover:text-slate-900 disabled:opacity-40 disabled:hover:bg-transparent " +
|
||||
"dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white";
|
||||
|
||||
@@ -85,7 +98,7 @@ export function Segmented<T extends string>({
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
className="flex rounded-lg border border-slate-300 p-0.5 dark:border-slate-700"
|
||||
className={`flex ${CONTROL_H} items-center rounded-lg border border-slate-300 p-0.5 dark:border-slate-700`}
|
||||
>
|
||||
{options.map((o) => {
|
||||
const active = o.value === value;
|
||||
@@ -94,7 +107,7 @@ export function Segmented<T extends string>({
|
||||
key={o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={
|
||||
"rounded-md px-2 py-1 text-[11px] font-medium transition-colors cursor-pointer " +
|
||||
"h-full rounded-md px-2.5 text-[12px] font-medium transition-colors cursor-pointer " +
|
||||
(active
|
||||
? "bg-slate-900! text-white! dark:bg-white! dark:text-slate-900!"
|
||||
: "text-slate-500 hover:bg-slate-100 hover:text-slate-900 " +
|
||||
@@ -172,7 +185,7 @@ export function Dialog({
|
||||
{onConfirm && (
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`${destructive ? BTN_DANGER + " py-2" : BTN_PRIMARY} cursor-pointer`}
|
||||
className={`${destructive ? BTN_DANGER : BTN_PRIMARY} cursor-pointer`}
|
||||
>
|
||||
{confirmLabel ?? "Continue"}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user