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:
24
src/App.css
24
src/App.css
@@ -5344,17 +5344,31 @@ body.has-player .chat-fab {
|
|||||||
|
|
||||||
.playlist-detail-header-top {
|
.playlist-detail-header-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
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 {
|
.playlist-detail-img {
|
||||||
flex-shrink: 0;
|
|
||||||
width: 72px;
|
|
||||||
height: 72px;
|
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: var(--radius-lg);
|
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 {
|
.playlist-detail-title-row {
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ export function PlaylistEditForm(
|
|||||||
? playlistImageUrl(playlist, mediaToken)
|
? playlistImageUrl(playlist, mediaToken)
|
||||||
: null)}
|
: null)}
|
||||||
alt="Cover"
|
alt="Cover"
|
||||||
size={72}
|
className="playlist-cover-banner"
|
||||||
|
borderRadius="var(--radius-lg)"
|
||||||
onChange={(file) => {
|
onChange={(file) => {
|
||||||
field.onChange(file);
|
field.onChange(file);
|
||||||
setImagePreview(URL.createObjectURL(file));
|
setImagePreview(URL.createObjectURL(file));
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ export interface ImagePickerProps {
|
|||||||
/** Shows a "+" placeholder when null. */
|
/** Shows a "+" placeholder when null. */
|
||||||
src: string | null;
|
src: string | null;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
/** Sets the side, in pixels. */
|
/** Sets the side, in pixels. Without it, `className` sizes the picker. */
|
||||||
size?: number;
|
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. */
|
/** Called with the file picked. The same file can be picked again. */
|
||||||
onChange: (file: File) => void;
|
onChange: (file: File) => void;
|
||||||
/** Disables the picker and shows "…" while the caller uploads. */
|
/** Disables the picker and shows "…" while the caller uploads. */
|
||||||
@@ -35,7 +36,7 @@ export interface ImagePickerProps {
|
|||||||
export function ImagePicker({
|
export function ImagePicker({
|
||||||
src,
|
src,
|
||||||
alt = "",
|
alt = "",
|
||||||
size = 80,
|
size,
|
||||||
borderRadius = 8,
|
borderRadius = 8,
|
||||||
onChange,
|
onChange,
|
||||||
uploading = false,
|
uploading = false,
|
||||||
@@ -43,7 +44,9 @@ export function ImagePicker({
|
|||||||
className,
|
className,
|
||||||
}: ImagePickerProps): React.JSX.Element {
|
}: ImagePickerProps): React.JSX.Element {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const sizeStyle = { width: size, height: size, borderRadius };
|
const sizeStyle = size === undefined
|
||||||
|
? { borderRadius }
|
||||||
|
: { width: size, height: size, borderRadius };
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
if (!uploading) inputRef.current?.click();
|
if (!uploading) inputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user