v4: a playlist's cover is a banner over its header, and the edit form picks it in the same shape — the cover was a 72px square beside the title, which left the title and description a narrow column under a mostly empty band: on a phone, in Kanagawa's wide mono, a long title broke into six lines. The cover now spans the header's width above the title, cropped to 3:1 and no taller than 14rem, inside the header's padding so outlined themes keep their ring around it; the title, buttons, description and meta take the whole width under it. The edit form's Cover picker takes the same shape (.playlist-cover-banner, shared with .playlist-detail-img), and with no cover is a dashed 3:1 placeholder. ImagePicker's size is now optional, so a class can size it, and its radius takes any CSS length, here --radius-lg; a picker sized by aspect-ratio sets min-height: 0, or its image would hold it at its own height. 669 frontend tests pass, and tsc, eslint, deno fmt and doc:lint are clean; checked in screenshots in Kanagawa dark (French, 385px), Brutalist at 390px with and without a cover, and Smooth at 1100px, the page and the form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-08 22:10:06 +00:00
parent 5acdf79c8d
commit 10c9cf4269
3 changed files with 28 additions and 10 deletions

View File

@@ -5344,17 +5344,31 @@ body.has-player .chat-fab {
.playlist-detail-header-top {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 1rem;
}
/* The cover is a banner over the title, cropped to 3:1 and no taller than
14rem on wide screens, and the edit form's picker takes the same shape. It
stays inside the header's padding, so themes that outline panels with
--panel-ring keep their ring around it. */
.playlist-detail-img,
.playlist-cover-banner {
display: block;
width: 100%;
aspect-ratio: 3 / 1;
max-height: 14rem;
}
.playlist-detail-img {
flex-shrink: 0;
width: 72px;
height: 72px;
object-fit: cover;
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
}
/* Or the picker's image would hold it at its own height: a box sized by
aspect-ratio grows to fit its content. */
.playlist-cover-banner {
min-height: 0;
}
.playlist-detail-title-row {

View File

@@ -144,7 +144,8 @@ export function PlaylistEditForm(
? playlistImageUrl(playlist, mediaToken)
: null)}
alt="Cover"
size={72}
className="playlist-cover-banner"
borderRadius="var(--radius-lg)"
onChange={(file) => {
field.onChange(file);
setImagePreview(URL.createObjectURL(file));

View File

@@ -16,9 +16,10 @@ export interface ImagePickerProps {
/** Shows a "+" placeholder when null. */
src: string | null;
alt?: string;
/** Sets the side, in pixels. */
/** Sets the side, in pixels. Without it, `className` sizes the picker. */
size?: number;
borderRadius?: number;
/** In pixels when a number, or any CSS length, such as a radius token. */
borderRadius?: number | string;
/** Called with the file picked. The same file can be picked again. */
onChange: (file: File) => void;
/** Disables the picker and shows "…" while the caller uploads. */
@@ -35,7 +36,7 @@ export interface ImagePickerProps {
export function ImagePicker({
src,
alt = "",
size = 80,
size,
borderRadius = 8,
onChange,
uploading = false,
@@ -43,7 +44,9 @@ export function ImagePicker({
className,
}: ImagePickerProps): React.JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
const sizeStyle = { width: size, height: size, borderRadius };
const sizeStyle = size === undefined
? { borderRadius }
: { width: size, height: size, borderRadius };
const pick = () => {
if (!uploading) inputRef.current?.click();
};